Các bạn có thể xem loạt clip về SOLID tại đây: https://www.youtube.com/playlist?list=PLRLJQuuRRcFlRei0t0wbbCdzU8ujTD3jE
Giờ ta đã có các business entity quan trọng nhất, cũng như định nghĩa ISolver, một “bản hướng dẫn” cách giải quyết vấn đề. Đúng vậy, ISolver là một interface, tự nó không thể giải được bài toán, nhưng nó đóng vai là bản cam kết, là một “contract” cho việc giải bài toán PTB1. Và đó cũng là nhiệm vụ của bất kỳ interface nào. Nguyên tắc đơn giản là: tôi không biết anh là ai, nhưng nếu anh implement ISolver, vậy chắc chắn anh có thể giải được bài toán này, anh có thể dùng đầu óc tính toán thông qua các phép cộng trừ nhân chia để tìm ra kết quả, hoặc như trong trường hợp của DistributedSolver (xem phần 2), anh phân phối lại các yêu cầu cho các máy tính khác và tổng hợp lại.
Vậy ta sẽ cần ít nhất một lớp hiện thực hóa ISolver, ta đặt tên nó là LocalCpuSolver (đặt tên hoành tráng một chút mới bán được nhiều tiền):
public interface ISolver {
Result Solve(Constants constants);
}
public class LocalCpuSolver implements ISolver {
Result Solve(Constants constants) {
return new Result() {
...
};
}
}
Tới đây, ta đã có một ISolver cụ thể (chính là LocalCpuSolver), hoàn toàn độc lập, bạn có thể viết các unit test và gắn vào để chạy, việc LocalCpuSolver chỉ phụ thuộc duy nhất vào ISolver giúp chúng ta dễ dàng kiểm thử, dễ dàng thay thế các instance của nó với instance của một class khác. Một thiết kế có càng ít mối phụ thuộc càng dễ bảo trì, chỉnh sửa, thay đổi… vậy nên, khi thiết kế, hãy cố gắng làm mọi thứ thật đơn giản.
Tương tự, ta sẽ xây dựng hai lớp ConsoleResultWriter và ConsoleConstantsReader, cho các interface IResultWriter và ConstantsReader (xem lại bài 2).
Dependency Inversion
Đây là quy tắc thứ năm trong SOLID, khi mà chúng ta đảo ngược các mối quan hệ phụ thuộc. Những gì chúng ta đang làm chính là Dependency Inversion, tôi sẽ giải thích tại sao.
Trước hết, chúng ta nhớ lại: các entity (Result, Constants) và use case (ISolver, IResultWriter, IConstantsReader) chúng ta định nghĩa trước đây được coi là linh hồn của chương trình, bởi chúng định nghĩa những gì khách hàng cần làm để giải quyết bài toán của họ. Nhưng vấn đề là, các thành phần này không thực sự giải quyết bài toán, chúng ta cần phải có ConsoleConstantsReader hay LocalCpuSolver để có thể hoạt động được, tức là chúng ta – một cách tự nhiên – phụ thuộc vào các thành phần cụ thể này.
Chúng ta luôn luôn có xu hướng xây dựng các thành phần cụ thể này trước, rồi sau đó tạo ra các thành phần khác phụ thuộc vào chúng. Tôi đã từng đặt một câu hỏi: có phải khi nhận được một yêu cầu, thứ đầu tiên xuất hiện trong đầu các bạn là sẽ thiết kế database như thế nào không? Đó chính là “non-Dependency Inversion”, và trong nhiều trường hợp không phải là cách làm tốt. Vậy tại sao chúng vẫn chạy? Và nhiều dự án vẫn hoạt động hàng ngày, có thể là phục vụ hàng chục hàng trăm khách hàng? Tôi muốn nói rằng, nó vẫn là một cách giải quyết, nhưng có thể đã có một cách khác tốt hơn, cách làm tốt hơn đó không hẳn sẽ nhanh hơn, nhưng sẽ giúp bạn giảm chi phí bảo trì, đặc biệt khi phát sinh thêm những bài toán mới.
Một thiết kế tốt là một thiết kế phục vụ cho những vấn đề trong tương lai, không phải cho hiện tại.
Dependency Inversion yêu cầu các mối quan hệ xảy ra theo chiều ngược lại, khi bạn thiết kế các contract, tức các interface, và các thành phần khác phải phụ thuộc vào chúng. Những thứ như dùng database gì, chạy trên web hay trên desktop, thậm chí có sử dụng các microservice hay không, lại là những vấn đề cuối cùng phải suy nghĩ đến, vì đi theo thứ tự ngược lại như vậy nên khi bạn thay đổi từ hệ CSDL này sang hệ CSDL khác, bạn chỉ cần phải thay đổi các lớp ở vòng ngoài cùng (trong hình minh họa).
Hướng quan hệ một chiều
Trong hình minh họa, các bạn có thể thấy các mối quan hệ sẽ theo hướng từ ngoài vào: các đối tượng trong vòng Interface Adapter phụ thuộc vào Use Case, Use Case phụ thuộc Entity. Những đối tượng thuộc vòng bên trong không cần biết về sự tồn tại của các đối tượng ở vòng ngoài, nhờ vậy các thay đổi ở vòng ngoài sẽ không ảnh hưởng đến vòng trong.
Cách thiết kế và sắp xếp các thành phần kiểu này được gọi là Clean Architecture.
Tổng kết
Có một điều có thể các bạn đã để ý thấy, khi nói về Open Close, tôi sẽ phải nói luôn cả về các nguyên tắc khác. Tương tự cho OOP, thật khó để chỉ nói đến một tính chất duy nhất, bởi tất cả các tính chất hay nguyên tắc đó nếu chỉ đặt riêng một mình sẽ chỉ mang lại được rất ít lợi ích.
Ta đã nói đến Open Close, để Open Close được, chúng ta sẽ phải cắt nhỏ các bước ra sao cho mỗi đối tượng/thao tác chỉ thực hiện đúng một nhiệm vụ duy nhất, nhờ cắt nhỏ ra ta mới có thể thay thế các thành phần với nhau được. Và cũng nhờ cắt nhỏ như vậy ta mới có thể dễ dàng mở rộng được, bởi mỗi thành phần đều đã được chuẩn hóa thông qua các interface.
Hi vọng qua loạt bài về Open Close này, các bạn có thể có một cách nhìn khác trong quá trình thiết kế.
Link bài 1: https://daohainam.com/2023/10/24/open-close-principle-phan-1/
Link bài 2: https://daohainam.com/2023/10/24/open-close-principle-phan-2/

One thought on “Open Close Principle – phần cuối”