SOLID - Đóng/ Mở (P2)
Phần này tôi muốn đưa tới các bạn một ví dụ thực tế về Open/Close, một tình huống cân đo đong đếm giữa Extend hay là Modify. Cùng xem biểu đồ lớp dưới đây mô tả giải pháp cho chức năng chuyển đổi một model thành một trang markup (html hay xml ...). Đây là một phần code trong dự án của tôi, bao gồm một phần code cũ và một phần code mới được refactor. PageParser là phần code cũ, được sử dụng ở nhiều nơi, trong khi phần NodeParser là phần mới được refactor (dù mới nhưng có thể là 1,2 năm trước đây ^^!). NodeParser trước đây được viết gói gọn trong một hàm gần 1000 dòng trong PageParser. Khi refactor PageParser, tôi đã quyết định chỉ tách NodeParser ra vì NodeParser chỉ được sử dụng bên trong PageParser, còn PageParser thì để lại sau vì nó được sử dụng ở nhiều nơi, refactor PageParser sẽ phát sinh nhiều vấn đề khác và cần test lại cả hệ thống (mặc dù là để lại sau nhưng cũng đã khá lâu chưa được xử lý!!!).
Gần đây phát sinh một yêu cầu, với điều kiện cụ thể thì LinkNodeParser không cần hoạt động, trả về null ngay lập tức. Quá trình mà tôi thực hiện như sau:
- Thêm đoạn code kiểm tra trong LinkNodeParser, nếu ParsingOptions có điều kiện yêu cầu thì return null. ParsingOptions cũng được thêm lựa chọn tương ứng
- Sửa danh sách tham số của hàm Parse của PageParser, thêm một tham số byPassLinkNodeParser với giá trị mặc định là false.
- Khi sửa tham số của PageParser.Parse, tôi cũng phải sửa lại các class khác sử dụng PageParser (dù không phải tất cả, chỉ các lời gọi có byPassLinkNodeParser = true).
- Tôi nhận này việc sửa đổi các lớp khác không thực sự liên quan có phần rủi ro, không chắc chắn nên tìm cách để không phải sửa chúng mà chỉ sửa PageParser.
- Sau đó tôi cũng nhận thấy mình quay lại điều kiện refactor PageParser cách đây không lâu, và lại từ bỏ ý định vì nó yêu cầu nhiều nguồn lực hơn đáng kể so với điều kiện hiện tại (chỉ cập nhật thêm chức năng nhỏ theo yêu cầu từ PO).
- Trước khi tạo Pull Request, tôi nhận ra rằng mình không cần phải sửa LinkNodeParser. Với thiết kế hiện tại, tạo một ByPassLinkNodeParserDecorator và sửa NodeParserFactory để decorate LinkNodeParser. Bằng cách này tôi bảo vệ được nghiệp vụ chính của LinkNodeParser và tự tin hơn với thay đổi của mình.
Quá trình trên thể hiện điều gì?
Dù là mở rộng hay sửa đổi thì đều có sửa đổi
Bạn sẽ vẫn phải sửa chỗ nào đó trong mọi tình huống. Thứ bạn cố gắng không sửa và mở rộng là nghiệp vụ chính, core business logic, use cases. Như ở trên, bạn vẫn phải sửa NodeParserFactory, nhưng nó là một lớp tiện ích không tham gia vào core business logic. Cái này có thể là một gợi ý khi bạn thiết kế: trong một tá các lớp được vẽ ra, hãy cố gắng bảo vệ các lớp thực hiện nghiệp vụ - hay các lớp nằm trong phát biểu của use cases.
Bạn muốn mở rộng không có nghĩa là bạn nên mở rộng
Công việc của chúng ta mang tính kĩ nghệ, thử - lỗi - giải quyết, đề cao hiệu quả và tối thiểu chi phí. Có những đoạn code dù xấu nhưng hoàn chỉnh, vì chúng đã vượt qua các phép kiểm thử và thực tế vận hành. Refactor lại chúng cần nỗ lực và cam kết lớn từ đội nhóm. Việc quyết định giải pháp nào đôi khi cần sự đánh giá của cả tập thể. Nếu impacts nhiều, bạn nên phất cờ, đánh giá cùng đội xem có thể thực hiện hay tách ra một yêu cầu khác để làm sau này. Đối với các nhóm scrum, những nhiệm vụ thế này thường không được ưu tiên ngay vì ảnh hưởng lớn đến story points của sprint.
Nguyên tắc này tôi muốn nhấn mạnh lại, là một nguyên tắc về thiết kế. Khi bạn đã thiết kế để mở rộng thì bạn mới có thể mở rộng mà không sửa đổi. Tôi đã phải tốn công đánh giá những rủi ro mà việc cố gắng mở rộng PageParser vì nó không sẵn sàng cho mở rộng. Nhưng mọi việc lại đơn giản với NodeParser.
Thiết kế tốt sẽ tự hướng bạn đến với Open/Close
Tôi không có ý định decorate LinkNodeParser ngay từ đầu vì chỉ thêm vào 1 dòng code đơn giản. Khi review lại code tôi nhận thấy mình có thể decorate LinkNodeParser từ thiết kế sẵn có. Giải pháp để Open/Close đã ở đó sẵn chứ không phải được phát minh ra khi chúng ta động đến code. Tât nhiên, việc đặt ưu tiên mở rộng trong tư duy của bạn là yêu cầu tiên quyết.
Việc tạo một decorator cho một dòng code có vẻ hơi quá tay, nhưng có một điều quan trọng hơn là "Tiền lệ". Nó như một lời nhắc nhở hay hướng dẫn cho những lần cập nhật sau.
===
Hi vọng với một tình huống thực tế như trên sẽ giúp các bạn hiểu rõ hơn về Open/Close và tìm được con đường áp dụng nó.
All rights reserved