ข้ามไปยังเนื้อหา

Designing Methods

method signature คือคำสัญญา พอ client พึ่งพา signature นี้แล้ว คุณเปลี่ยนได้แค่ในทางที่ backward-compatible เท่านั้น — หรือไม่ก็ bump ทั้ง package เป็น v2 ดังนั้นนิสัยการออกแบบด้านล่างไม่ใช่แค่ขัดเงา แต่คือสิ่งที่ทำให้ API อยู่รอดผ่านการเปลี่ยนแปลงเป็นปี ๆ โดยไม่ต้อง migration ที่เจ็บปวด

กฎที่มีค่าที่สุด: ทุก 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 หรือ timestamp deleted_at ใน response เป็นการเปลี่ยนแบบ additive ที่ปลอดภัย scalar เปล่า ๆ หรือ Empty ที่ใช้ร่วมกันไม่มีที่ให้เพิ่ม
  • ชัดเจน DeleteUserRequest บอกเจตนาได้ดีกว่า int64
  • สม่ำเสมอ ทุก method หน้าตาเหมือนกัน tooling และคนอ่านจึงไม่ต้อง handle เคสพิเศษเลย

ทำตาม convention ที่ code generator คาดหวัง แล้ว client ที่ generate ออกมาจะอ่านเป็นธรรมชาติในทุกภาษา:

  • method: VerbNoun แบบ PascalCaseGetUser, CreateOrder, ListInvoices, BatchUpdateItems verb มาตรฐาน (Get, List, Create, Update, Delete) ตั้งความคาดหวังได้ชัดเจน
  • message: MethodNameRequest / MethodNameResponseListUsersRequest, ListUsersResponse
  • field: snake_caseuser_id, page_size, created_at generator แปลงเป็น casing ของแต่ละภาษาให้

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"]
pagination แบบ token: แต่ละ response ยื่นกุญแจของหน้าถัดไปให้คุณ

client ส่ง next_page_token ที่ได้กลับมาไปเรื่อย ๆ จนกว่าจะกลับมาเป็นค่าว่าง token นี้ opaque — server ตัดสินใจเองว่าจะ encode อะไร (offset, cursor, keyset) และ client ต้องไม่ parse token นี้เด็ดขาด อิสระนั้นทำให้คุณเปลี่ยน strategy การ pagination ทีหลังได้โดยไม่แตะ client แม้แต่ตัวเดียว

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 ต้องการตัวช่วยแบบนี้

ทำไมต้อง wrap ทุก method ด้วย request/response message ของตัวเอง แม้แต่ตัวที่ว่าง?
casing ที่แนะนำสำหรับชื่อ field ใน protobuf คืออะไร?
ทำไม page_token ถึงต้อง opaque สำหรับ client?
โดยทั่วไปทำให้ Create method retry ได้อย่างปลอดภัยยังไง?