Designing Methods
API ที่คุณ ship ออกไปคือตลอดกาล
หัวข้อที่มีชื่อว่า “API ที่คุณ ship ออกไปคือตลอดกาล”method signature คือคำสัญญา พอ client พึ่งพา signature นี้แล้ว คุณเปลี่ยนได้แค่ในทางที่ backward-compatible เท่านั้น — หรือไม่ก็ bump ทั้ง package เป็น v2 ดังนั้นนิสัยการออกแบบด้านล่างไม่ใช่แค่ขัดเงา แต่คือสิ่งที่ทำให้ API อยู่รอดผ่านการเปลี่ยนแปลงเป็นปี ๆ โดยไม่ต้อง migration ที่เจ็บปวด
wrap request และ response เสมอ
หัวข้อที่มีชื่อว่า “wrap request และ response เสมอ”กฎที่มีค่าที่สุด: ทุก method มี request message และ response message ของตัวเอง อย่ารับหรือ return scalar เปล่า ๆ และอย่า reuse domain message มาเป็น request
// ทำแบบนี้rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse);
message DeleteUserRequest { int64 id = 1; }message DeleteUserResponse {} // empty today, has room tomorrow// เลี่ยงแบบนี้rpc DeleteUser(int64) returns (google.protobuf.Empty);ทำไมต้อง wrap ทั้งที่วันนี้ message ยังว่างอยู่?
- มีที่ให้โต การเพิ่ม flag
soft_deleteเข้าไปใน request หรือ timestampdeleted_atใน response เป็นการเปลี่ยนแบบ additive ที่ปลอดภัย scalar เปล่า ๆ หรือEmptyที่ใช้ร่วมกันไม่มีที่ให้เพิ่ม - ชัดเจน
DeleteUserRequestบอกเจตนาได้ดีกว่าint64 - สม่ำเสมอ ทุก method หน้าตาเหมือนกัน tooling และคนอ่านจึงไม่ต้อง handle เคสพิเศษเลย
Convention การตั้งชื่อ
หัวข้อที่มีชื่อว่า “Convention การตั้งชื่อ”ทำตาม convention ที่ code generator คาดหวัง แล้ว client ที่ generate ออกมาจะอ่านเป็นธรรมชาติในทุกภาษา:
- method:
VerbNounแบบ PascalCase —GetUser,CreateOrder,ListInvoices,BatchUpdateItemsverb มาตรฐาน (Get,List,Create,Update,Delete) ตั้งความคาดหวังได้ชัดเจน - message:
MethodNameRequest/MethodNameResponse—ListUsersRequest,ListUsersResponse - field:
snake_case—user_id,page_size,created_atgenerator แปลงเป็น casing ของแต่ละภาษาให้
Pagination: อย่า return list ที่ไม่มีขอบเขต
หัวข้อที่มีชื่อว่า “Pagination: อย่า return list ที่ไม่มีขอบเขต”ListUsers ที่ return user ทั้งหมด สุดท้ายจะ return เป็นล้านแล้วล่ม ออกแบบ list method ให้ page ตั้งแต่แรกด้วย token pattern ที่ใช้กันแพร่หลาย:
message ListUsersRequest { int32 page_size = 1; // how many to return string page_token = 2; // opaque cursor from the previous response}
message ListUsersResponse { repeated User users = 1; string next_page_token = 2; // empty when there are no more}flowchart LR r1["ListUsers(page_size=50)"] --> p1["50 users + next_page_token=abc"] p1 --> r2["ListUsers(token=abc)"] r2 --> p2["next 50 + next_page_token=def"] p2 --> r3["... until token is empty"]
client ส่ง next_page_token ที่ได้กลับมาไปเรื่อย ๆ จนกว่าจะกลับมาเป็นค่าว่าง token นี้ opaque — server ตัดสินใจเองว่าจะ encode อะไร (offset, cursor, keyset) และ client ต้องไม่ parse token นี้เด็ดขาด อิสระนั้นทำให้คุณเปลี่ยน strategy การ pagination ทีหลังได้โดยไม่แตะ client แม้แต่ตัวเดียว
Idempotency: ทำให้ retry ปลอดภัย
หัวข้อที่มีชื่อว่า “Idempotency: ทำให้ retry ปลอดภัย”network ล่ม และ client retry ถ้า CreatePayment ที่ถูก retry ชาร์จลูกค้าสองรอบ นั่นคือบั๊กด้านการออกแบบ ไม่ใช่โชคร้าย ทำให้ mutating method idempotent เท่าที่ทำได้ — วิธีที่พบบ่อยคือใช้ request id ที่ client สร้างขึ้นแล้ว server dedup:
message CreatePaymentRequest { Payment payment = 1; string request_id = 2; // client-generated; server ignores duplicates}server บันทึกแต่ละ request_id; ถ้าซ้ำก็ return ผลเดิมแทนที่จะชาร์จอีกครั้ง Get/List เป็น idempotent อยู่แล้วโดยธรรมชาติ; Delete มักจะใช่; Create ต้องการตัวช่วยแบบนี้