Nguyen Le PhongNguyen Le Phong

Software Architecture FoundationsPhần 14/15

Dependency direction bằng ngôn ngữ đời thường

Dependency direction là một quy tắc đơn giản giúp team thống nhất: business rule ổn định không nên phụ thuộc vào chi tiết delivery dễ đổi. Bài viết dùng một tình huống release nhỏ để giải thích vì sao hướng phụ thuộc giúp code, review và trao đổi trong team bình tĩnh hơn.

Pull request ban đầu nhìn khá nhỏ. Product manager muốn thêm một rule cho refund, một developer mở vài file, rồi buổi review kéo dài gần hết buổi chiều. Rule nằm một phần trong API controller, một phần trong database query, một phần trong shared helper, và một phần trong test fixture mà không ai muốn đụng vào. Đến lúc cả team hiểu được thay đổi, phần business decision thật ra lại là phần dễ nhất.

Đây là lúc dependency direction trở nên hữu ích. Nó không phải một khẩu hiệu kiến trúc xa vời. Nó là một thỏa thuận rất đời về việc ai được phép biết điều gì. Rule refund nên biết ngôn ngữ của refund. Nó có thể biết order được approve, reject, hết hạn, hay đã settle. Nó không nên cần biết HTTP framework nào nhận request, ORM nào load dữ liệu, queue nào bắn notification, hay payment provider SDK nào đang được dùng trong quý này.

Nói đơn giản, quy tắc là thế này: code thay đổi vì lý do business sâu hơn không nên phụ thuộc vào code thay đổi vì chi tiết delivery. Ý tưởng cốt lõi nên nằm ở phần yên tĩnh hơn của hệ thống. Những adapter xung quanh có thể đổi khi thế giới bên ngoài đổi. API shape có thể đổi. Database library có thể đổi. Vendor có thể đổi tên field. Rule không nên phải di chuyển mỗi khi một chi tiết như vậy di chuyển.

Hãy tưởng tượng một team nhỏ đang ship refund feature. Product manager quan tâm đến policy: order nào được refund, cửa sổ refund kéo dài bao lâu, chuyện gì xảy ra khi hàng đã shipped, và khi nào cần người duyệt. Backend developer quan tâm đến request validation, persistence, provider call và event. QA quan tâm đến scenario. Support quan tâm đến thông báo người dùng nhìn thấy. Nếu tất cả bị trộn trong cùng vài file, mỗi cuộc nói chuyện đều trở nên chật chội. Không ai chắc mình đang review refund policy hay đường ống vận chuyển policy đó.

Khi hướng phụ thuộc rõ hơn, team có thể vẽ một đường bình tĩnh hơn. Refund policy trở thành thứ được các phần khác gọi vào. Nó có thể expose một function hoặc use case như requestRefund. Bên ngoài nó, web handler chuyển HTTP request thành input. Database adapter load order. Payment adapter gọi provider để reverse money. Notification adapter gửi message. Các phần bên ngoài phụ thuộc vào refund rule vì chúng phục vụ rule đó. Refund rule không phụ thuộc vào chúng vì nó không nên bị định hình bởi những chi tiết tạm thời của chúng.

Điều này không có nghĩa core quan trọng hơn mọi phần còn lại. Người dùng vẫn cần route, screen, log, retry và message. Nó chỉ nói rằng các loại code khác nhau có nhịp thay đổi khác nhau. Business language nên được bảo vệ khỏi những cạnh ồn ào. Edge nên dễ thay thế vì edge là nơi thế giới bên ngoài liên tục thương lượng với mình: SDK mới, auth header mới, event name mới, table mới, UI flow mới, quyết định hạ tầng mới.

Lợi ích về testing thường là nơi ý tưởng này hiện ra rõ nhất. Nếu refund rule phụ thuộc trực tiếp vào database thật và payment SDK thật, mỗi test phải kéo những thứ đó vào phòng. Team dần viết ít test hơn vì test chậm, mong manh hoặc khó setup. Nếu rule chỉ phụ thuộc vào interface nhỏ hoặc input data đơn giản, team có thể test policy trong vài phút: order được refund, cửa sổ đã hết hạn, hàng đã shipped, request bị lặp, cần manual approval. Chi tiết delivery vẫn cần test, nhưng chúng không còn chặn team hiểu rule chính.

Lợi ích trong review thì lặng hơn nhưng rất đáng giá. Một reviewer có thể hỏi: đây có đúng policy đã thống nhất không? Một reviewer khác có thể hỏi: adapter này xử lý provider đủ an toàn chưa? Hai câu hỏi đó khác nhau. Khi hướng phụ thuộc rõ, pull request kể một câu chuyện sạch hơn. Team có thể bàn về product behavior mà không bị lạc trong framework ceremony, và có thể bàn về framework behavior mà không vô tình đổi product policy.

Dependency direction cũng giúp khi team không thống nhất từ vựng kiến trúc. Một người gọi là Clean Architecture, người khác gọi là Ports & Adapters, người khác nữa không thích cả hai tên. Trong công việc hằng ngày, từ vựng ít quan trọng hơn câu hỏi này: rule ổn định có đang biết về chi tiết dễ đổi không, hay chi tiết dễ đổi đang chỉ vào rule ổn định? Nếu rule import framework, hướng phụ thuộc có thể đang ngược. Nếu framework gọi rule qua một boundary nhỏ, hướng đó thường dễ hiểu hơn.

Dĩ nhiên vẫn có giới hạn thực tế. Một script rất nhỏ, một prototype ngắn hạn, hoặc một feature gần như không có policy đáng kể có thể không cần boundary trang trọng. Over-design có thể làm một thay đổi nhỏ giống như điền giấy tờ. Mục tiêu không phải bọc mọi dòng code trong abstraction. Mục tiêu là nhận ra những chỗ codebase làm team lặp lại cùng một cuộc nói chuyện mơ hồ. Khi một business rule đủ quan trọng để sống qua nhiều screen, vendor, job hoặc release cycle, nó xứng đáng có một nơi mà chi tiết bên ngoài không liên tục rò vào.

Nói đời thường, dependency direction là vệ sinh làm việc của team. Nó cho mọi người một câu trả lời chung cho câu hỏi nhỏ nhưng tốn kém: khi thứ này đổi, ai phải quan tâm? Nếu câu trả lời là tất cả mọi người, hệ thống sẽ tiếp tục làm những release nhỏ cảm giác lớn hơn thực tế. Nếu câu trả lời rõ, team có thể đổi phần edge mà không làm xáo core, và đổi core mà không giả vờ đó chỉ là chuyện edge. Sự rõ ràng đó hiếm khi xuất hiện sau một refactor kịch tính. Nó lớn lên qua nhiều lựa chọn nhỏ trong code review, cách đặt tên, test, và sự kiên nhẫn hỏi xem dependency có đang chỉ về phía thứ nên đứng vững hay không.

Nếu team bạn có một feature luôn khó sửa hơn business rule của nó gợi ý, có thể đáng mở các file ra và cùng nhau lần theo mũi tên. Không phải để trách thiết kế cũ, mà để xem chi tiết nào đang kéo cuộc trò chuyện ra khỏi quyết định team thật sự cần làm.

Bạn thấy bài viết thế nào?