No Abstractions: Nguyên tắc thiết kế API của Increase
(increase.com)- Increase cho rằng tài nguyên API quyết định cách người dùng hiểu sản phẩm, nên áp dụng nguyên tắc No Abstractions: bộc lộ thay vì che giấu độ phức tạp của các mạng thanh toán
- Kiểu trừu tượng hóa của Stripe mạnh ở khả năng tích hợp nhanh, nhưng người dùng Increase muốn kết nối trực tiếp và tích hợp sâu dựa trên kiến thức về payment network
- API dùng nguyên các thuật ngữ mạng nền tảng như Nacha specification, và mô hình hóa tiến trình của ACH transfer thành các đối tượng con bất biến
- Khi các hành động người dùng có thể thực hiện khác nhau đáng kể, Increase tách tài nguyên như
ach_transfervàinbound_ach_transfer; ban đầu có thể dài dòng nhưng về lâu dài tăng tính dự đoán - Mức trừu tượng hóa nên được quyết định theo kinh nghiệm miền và mức độ sẵn sàng đầu tư của nhà phát triển tích hợp; nếu đã chọn mức trừu tượng hóa thấp, cần tiếp tục duy trì nguyên tắc đó về sau
Tài nguyên API tạo nên mô hình tinh thần của người dùng
- API resource là danh từ của API; việc đặt tên và xác định mô hình là một trong những phần khó và quan trọng nhất của thiết kế API
- Việc phơi bày tài nguyên nào sẽ cấu thành mô hình tinh thần giúp người dùng hiểu cách sản phẩm vận hành và những việc họ có thể làm
- Increase dùng nguyên tắc thiết kế “No Abstractions” để hỗ trợ các quyết định này
-
Sự khác biệt giữa trừu tượng hóa kiểu Stripe và Increase
- Stripe có thế mạnh trong việc rút gọn miền thanh toán phức tạp thành một API mà người dùng có thể dễ dàng xử lý, tức trừu tượng hóa
- Stripe mô hình hóa nhiều mạng thanh toán thành một API resource tên là
PaymentIntent, đồng thời gộp khác biệt về chargeback reason code giữa Visa và Mastercard vào một enum duy nhất để người dùng không phải cân nhắc riêng từng mạng - Phần lớn người dùng Stripe là các startup giai đoạn đầu đang xây dựng sản phẩm không xoay quanh bản thân việc thanh toán; họ muốn tích hợp nhanh thay vì hiểu sâu chi tiết thẻ tín dụng, rồi quay lại phát triển sản phẩm chính
- Người dùng Increase có kiến thức sẵn có sâu về payment network, tiếp tục làm việc với công nghệ tài chính, và dùng Increase để kết nối mạng trực tiếp cũng như tích hợp sâu
- Họ muốn biết chính xác FedACH window đóng khi nào và transfer sẽ đến lúc nào, đồng thời hiểu rằng khi Standard Entry Class code của ACH transfer thay đổi thì return timing cũng có thể thay đổi
- Nếu gom ACH transfer và wire transfer vào một API resource để che giấu độ phức tạp của mạng nền tảng, với người dùng Increase đó không phải là đơn giản hóa mà là sự bất tiện
No Abstractions thể hiện trong API như thế nào
-
Sử dụng thuật ngữ mạng thực tế
- Increase thường dùng từ vựng của mạng nền tảng thay vì tự tạo tên mới cho API resource và attribute
- Khi tạo API cho ACH transfer, các parameter được phơi bày dựa theo tên field trong Nacha specification
-
Tài nguyên bất biến và lifecycle object
- Tài nguyên cũng được mô hình hóa theo các sự kiện hoặc thông điệp trong thế giới thực; cách tiếp cận này khiến nhiều API resource trở thành bất biến hơn
- Giống như một tập hợp các thông điệp mạng có thể gửi trong lifecycle của ACH transfer, Increase nhóm các tài nguyên bất biến dưới một lifecycle object dạng state machine
- Đối tượng
ach_transfercó fieldstatusthay đổi theo thời gian, cùng nhiều sub-object bất biến được tạo ra khi lifecycle tiến triển - Một
ach_transfermới có thể cóstatuslàpending_approval, cònapproval,submission,acknowledgementlànull - Sau khi được gửi tới FedACH,
statussẽ trở thànhsubmitted, vàapproval,submission,acknowledgementlần lượt được điền bằng thông tin bất biến tại thời điểm phê duyệt, gửi và xác nhận submissionbao gồm các giá trị nhưtrace_numbervàsubmitted_at
-
Tách tài nguyên theo từng trường hợp sử dụng
- Ngay cả với cùng một API resource, nếu tập hành động có thể thực hiện khác nhau đáng kể giữa các instance, Increase thường chia chúng thành nhiều tài nguyên
- Vì các hành động có thể thực hiện với originated ACH transfer và received ACH transfer gần như đối lập, chúng được tách thành
ach_transfervàinbound_ach_transfer - Cách này ban đầu có thể trông dài dòng và gây choáng ngợp hơn, đến mức phía bên trái tài liệu API hiển thị nhiều tài nguyên
- Đổi lại, về lâu dài quan hệ giữa tài nguyên và hành động trở nên dễ dự đoán hơn
Nguyên tắc giúp giảm các quyết định thiết kế nhỏ
- Khi thiết kế một API phức tạp trong nhiều năm, các quyết định nhỏ liên tục phát sinh; những nguyên tắc nền tảng đặt ra từ sớm giúp giảm tải nhận thức của các quyết định này
Input Message Accountability Datacần khi gửi wire transfer tới Federal Reserve đóng vai trò như ID duy nhất toàn cục của transfer đó- Với một API có nhiều trừu tượng hóa, kỹ sư có thể phải cân nhắc nên gọi nó theo cách “thân thiện với người dùng” hơn là
trace_number,reference_numberhayid - Ở Increase, field name được quyết định là
input_message_accountability_datarồi tiếp tục - Khi người dùng lần đầu thấy field này, tên của nó có thể không dễ nhận ra ngay, nhưng nó giúp họ hiểu tức thì cách field đó ánh xạ với hệ thống nền tảng
Tiêu chí khi quyết định mức trừu tượng hóa
- No Abstractions không phải là nguyên tắc phù hợp với mọi API
- Mức trừu tượng hóa phù hợp phụ thuộc vào kinh nghiệm miền của nhà phát triển tích hợp, mức hiểu biết về lĩnh vực sản phẩm, và năng lượng họ sẵn sàng dành cho việc tích hợp
- Nếu xây dựng một API có nhiều trừu tượng hóa, cần suy nghĩ sâu trước khi thêm tính năng mới
- Nếu xây dựng một API ít trừu tượng hóa, cần cam kết theo hướng đó và chống lại cám dỗ thêm trừu tượng hóa
1 bình luận
Ý kiến trên Hacker News
Luôn có thể cung cấp cả hai
Có thể cung cấp một API mức thấp cho phép kiểm soát chi tiết nhưng đòi hỏi chuyên môn sâu, và bên trên đó xây dựng một API mức cao ánh xạ các trường hợp sử dụng phổ biến thành vài thao tác đơn giản. Dù sao thì một số khách hàng có thể cũng đang tự triển khai vụng về lớp mức cao kiểu này rồi
Nếu tách bạch rõ ràng hai lớp, sẽ giảm áp lực phải đưa abstraction vào API mức thấp, hoặc thêm các vết sạn và trường hợp đặc biệt vào API mức cao. Nếu khách hàng muốn những thứ đó thì chúng đã tồn tại ở API còn lại rồi
Sẽ còn tốt hơn nếu cung cấp cả tài liệu giúp khách hàng học cách chuyển từ lớp này sang lớp kia. Như vậy có thể thu hút cả những khách hàng chưa hiểu sâu cấu trúc nội bộ của mạng thanh toán nhưng muốn phát triển theo hướng đó
Hôm nay tôi dùng Web File System API, và để ghi một chuỗi vào file thì cần tới 7 lần gọi hàm, hầu hết đều là bất đồng bộ. Còn chưa tính xử lý lỗi, và phải làm trong worker, mà ngay cả việc thiết lập worker cũng phiền phức tương tự. Có thể thấy sự kinh khủng tương tự ở IndexedDB, WebRTC, thao tác DOM thông thường, còn Vulkan, DirectX, ffmpeg thì tệ hơn nhiều
Để xử lý đủ loại trường hợp đặc biệt thì mức độ phức tạp đó phần nào là hợp lý, nhưng trong đa số trường hợp thì không phải vậy
Thiết kế API nên bắt đầu bằng việc phác thảo xem đoạn code sử dụng API cho các trường hợp phổ biến sẽ trông như thế nào, và các trường hợp đó phải đơn giản nhất có thể. Ví dụ, fetch API làm khá tốt điều này, còn XMLHttpRequest thì hoàn toàn không
https://developer.mozilla.org/en-US/docs/Web/API/FileSystemS...
Tôi đã nhiều lần nghĩ rằng sẽ rất hay nếu có một lớp API tiện ích thống nhất cho mọi Web API. Kiểu như bọc mọi tính năng mạnh mẽ trong các wrapper “thư viện chuẩn” nhất quán, ít nhất là hỗ trợ các trường hợp sử dụng phổ biến nhất. Trình duyệt hiện đại rất mạnh, nhưng vì thiết kế của từng API quá rời rạc và khó học hoặc khó dùng một cách không cần thiết, nên sức mạnh đó không được biết đến rộng rãi hoặc bị dùng ít đi
Tương tự như những gì jQuery đã làm với DOM, nhưng nên ít “ma thuật” hơn và cũng ít tính năng phụ hơn. node.js phần nào có API nhất quán, nhưng hơi cũ, ví dụ hỗ trợ Promise chỗ có chỗ không. Cũng tương tự cách Python theo đuổi các API “đậm chất Python”
Một khi đã quen với góc nhìn triển khai nội bộ của công cụ, rất dễ quên mất mọi người thực sự sử dụng nó như thế nào
Có các lệnh “porcelain” mức cao như branch và checkout, và các lệnh “plumbing” mức thấp như commit-tree và update-ref
https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Po...
Tôi thích phần giải thích vì sao Increase chọn một cách tiếp cận khác. Khi thiết kế những thứ nền tảng, bối cảnh là cực kỳ quan trọng, nhưng thường mọi người không thừa nhận điều đó đủ nhiều
Ở đây, “không trừu tượng hóa” thực chất có nghĩa là dùng nguyên thuật ngữ của hệ thống nền tảng, và nhìn chung đây là một nguyên tắc đặt tên tốt.
Vấn đề tất yếu nảy sinh theo thời gian khi có nhiều hệ thống nền tảng cùng tồn tại, bắt đầu gán những tên khác nhau cho cùng một thứ, hoặc tệ hơn là dùng cùng một tên cho những thứ khác nhau. Trong ví dụ này, nếu mô hình của các nhà cung cấp thanh toán nền tảng khác nhau thì phải làm sao? Và nếu Federal Reserve loại bỏ Input Message Accountability Data và thay bằng một khái niệm mới thì sao?
Ngành thanh toán có thể đơn giản hơn nhiều so với vận tải hay giao thức mạng. Nếu bạn từng xây dựng một sản phẩm chuyển mạch gói dựa trên X.25 rồi sau này muốn hỗ trợ cả TCP/IP, thì trừu tượng hóa đúng nên là gì?
Về vấn đề bị loại bỏ, may mắn là hệ thống nền tảng không thay đổi quá nhiều nên không sao. Input Message Accountability Data sẽ không biến mất. Nhưng chẳng hạn nếu chúng tôi bắt đầu phát hành thẻ không chỉ trên Visa mà cả Mastercard nữa, thì sẽ gặp xung đột.
Chúng tôi cũng đã thử nghiệm một vài trừu tượng hóa, và ở điểm đó điều này cũng có thể xảy ra. Một quy tắc mà chúng tôi luôn giữ là không trừu tượng hóa “đối tượng nền tảng”, mà thay vào đó đưa vào các tổ hợp cấp cao hơn để tiện dùng. Ví dụ, “Card Payment” thực ra không tồn tại(https://increase.com/documentation/api#card-payments). Nó chỉ là cách gom các thông điệp phê duyệt và thanh toán thẻ có liên quan lại với nhau. Nhưng nó rất hữu ích với người dùng và không dễ tự làm trực tiếp, nên chúng tôi đã thử. Tuy vậy, tôi nghĩ các thông điệp mạng nền tảng, tức “đối tượng nền tảng”, cùng toàn bộ các trường gốc vẫn phải có thể truy cập được trong API.
Tiếc là các API công khai tôi từng làm đều 100% thuộc lĩnh vực thanh toán, nên tôi rất muốn nghe thêm các góc nhìn khác.
Trong DDD, người ta thường đi theo tên gọi và mô hình khái niệm mà miền nghiệp vụ đã tạo ra. Nếu cố đưa vào mô hình hay thuật ngữ “được cải tiến” của riêng mình [0], sẽ phát sinh ma sát và hiểu lầm, tăng khả năng lỗi tích hợp, đồng thời phớt lờ tri thức chuyên môn đã được kiểm chứng qua hàng chục hay hàng trăm năm.
[0] https://xkcd.com/793/
Bài viết hay đấy.
Nếu bạn thích Stripe, thì với tư cách vừa là nhà thiết kế vừa là nhà sáng lập công nghệ, tôi cũng thấy sự đơn giản và năng lực frontend của Stripe thật đáng kinh ngạc, và nhìn họ rất dễ muốn bắt chước khả năng đơn giản hóa cũng như tạo ra trải nghiệm chỉn chu của họ.
Nhưng tay nghề thực sự của Stripe nằm ở chỗ họ hiểu khách hàng rất rõ. Và họ cũng hiểu rất rõ kiểu đơn giản mà khách hàng khao khát.
Qua bài này, Increase cũng cho cảm giác như vậy, và dường như đã tạo ra một bộ chỉ dẫn thiết kế sản phẩm rất tốt nhờ tập trung sắc bén tương tự vào việc khách hàng cần gì. Thật đáng khích lệ.
Cá nhân tôi thích vế sau hơn khi nó xảy ra, nhưng ở đó cũng có một phần là quyết định mang tính thẩm mỹ.
Điều này giống với mẫu thiết kế ngôn ngữ phổ quát trong thiết kế hướng miền. Đây là cách buộc việc triển khai phải dùng nguyên các thuật ngữ ngoài đời thực mà chuyên gia miền sử dụng.
https://thedomaindrivendesign.io/developing-the-ubiquitous-l...
Bài này khiến tôi đọc ra một kiểu phản ứng né tránh xấu hổ. Mọi người ghét bệnh hoạn việc nói “tôi đã sai” hoặc “chúng ta đã sai”, nên cứ đẩy ẩn dụ đi lòng vòng như một đứa trẻ di chuyển rau trên đĩa để trông như đã ăn chúng.
Nó cũng làm tôi nhớ đến câu “không có khuyết điểm hiển nhiên” trong bài phát biểu nhận giải Turing của Hoare.
Đây là một ví dụ rất rõ cho khái niệm ngôn ngữ phổ quát của thiết kế hướng miền.
Bạn phải dùng ngôn ngữ mà chuyên gia miền hiểu. Nếu người dùng biết NACHA file, thì ngay khi bạn dùng một thuật ngữ khác, họ sẽ phải giữ việc ánh xạ đó trong đầu.
Ngược lại, trong trường hợp của Stripe, người dùng không phải là chuyên gia miền, nên việc tạo ra các trừu tượng hóa vừa dễ hiểu vừa che đi các chi tiết không cần thiết là có giá trị. Nếu phải dạy ngôn ngữ cho người dùng, bạn nên làm nó đơn giản nhất có thể.
Nếu không có các trừu tượng hóa như POSIX, thì ứng dụng hẳn đã phải viết adapter cho từng hệ thống tệp mà nó hỗ trợ.
Thú vị đấy.
Tiêu đề của khái niệm này dễ gây hiểu lầm. Ở đây, “không trừu tượng hóa” không có nghĩa đen là hoàn toàn không có trừu tượng hóa, mà là “dùng tập hợp trừu tượng hóa cụ thể này, và không dùng những tập hợp khác”. Tập con cụ thể mà họ mô tả đáng để bàn, nhưng hiển nhiên vẫn là một tập hợp trừu tượng hóa.
Ví dụ, họ nói “khi tạo ACH transfer thành API, tên các tham số được lộ ra sẽ theo tên trường trong đặc tả Nacha”, nhưng bản thân đặc tả đã là một trừu tượng hóa.
Họ cũng nói “giống như dùng thuật ngữ mạng, chúng tôi cố mô hình hóa tài nguyên theo các sự kiện thực tế, chẳng hạn hành động đã được thực hiện hoặc thông điệp đã được gửi. Kết quả là nhiều tài nguyên API trở nên bất biến hơn và được nhóm dưới các ‘đối tượng vòng đời’ là máy trạng thái”, nhưng tính bất biến theo nghĩa này và “đối tượng vòng đời” cũng là trừu tượng hóa.
Việc “nếu trong một tài nguyên API cụ thể, tập hợp hành động mà người dùng có thể thực hiện trên từng instance khác nhau quá nhiều thì chúng tôi có xu hướng tách ra thành nhiều tài nguyên” cũng là một trừu tượng hóa khác. Chỉ là nó được chia ở mức khác so với Stripe API.
Rốt cuộc đây là một tập hợp các quyết định thiết kế và trừu tượng hóa, chứ không phải nguyên tắc “không trừu tượng hóa”. Quyết định quan trọng nhất có vẻ là tổng quát hóa ít nhất có thể, mà bản thân tổng quát hóa cũng là một dạng trừu tượng hóa. Có lẽ “ít tổng quát hóa hơn” sẽ là tiêu đề chính xác hơn.
Tôi thấy đoạn “phí hàng tháng cho mỗi người dùng xây dựng trên Increase sẽ khác nhau tùy theo trường hợp sử dụng”.
Hiện tôi đang thêm quyền truy cập API công khai cho endpoint AI text-to-SQL hỗ trợ RAG, và vấn đề lớn nhất là định giá. Có ai biết đại khái nên ở mức giá nào không? Giá cần phản ánh token OpenAI, hoặc phương án để người dùng tự cung cấp token OpenAI của họ, mức sử dụng cơ sở dữ liệu, và trong tương lai còn phải tính cả cache cùng cấu hình giới hạn tốc độ nữa
Ví dụ, theo tôi biết thì Gong tính phí hơn 100.000 USD mỗi năm với nhiều tổ chức; ngay cả khi tính lưu trữ, CPU và các chi phí vận hành khác, chi phí đó cũng không thể nào gần với chi phí tính toán. Có lẽ chênh lệch ít nhất cũng phải ở mức một chữ số lần. Nhưng vì đội ngũ bán hàng tạo ra doanh thu rất trực tiếp, nên đòn bẩy mà một công cụ như Gong mang lại có giá trị tức thời, rõ ràng và đáng để trả tiền
[1]: Một ngoại lệ của nguyên tắc tránh định giá cộng biên theo chi phí là khi bạn bán hàng hóa phổ thông. Nhưng bạn không ở trong trường hợp đó!