Open – Close Principle – Phần 2


Xem bài 1 tại đây: https://daohainam.com/2023/10/24/open-close-principle-phan-1/

Các bạn có thể xem loạt clip về SOLID tại đây: https://www.youtube.com/playlist?list=PLRLJQuuRRcFlRei0t0wbbCdzU8ujTD3jE

Trước hết, vấn đề chúng ta cần phải giải quyết là gì? Đó là tìm ra nghiệm của một phương trình bậc 1 với đầu vào là a và b cho trước, kết quả có thể là vô nghiệm, vô số nghiệm hoặc một x cụ thể nào đó.

Điều tôi muốn nhấn mạnh ở đây chính là mục đích mà chương trình được tạo ra. Thực chất khách hàng của bạn có quan tâm tới việc dữ liệu đầu vào đến từ bàn phím hay từ file không? Không! Họ có quan tâm dữ liệu sẽ được đưa ra màn hình, file PDF hay Excel không? Cũng không! Việc tìm nghiệm mới là mục đích tồn tại của chương trình này, mọi thứ khác diễn ra xung quanh và chỉ nhằm phục vụ cho mục đích này mà thôi.

Khi nhìn vào một bài toán, chúng ta sẽ phải tìm ra những thành phần tham gia vào hệ thống, cái nào quan trọng hơn cái nào, cái nào thường xuyên và cái nào không bao giờ thay đổi… Xác định đúng các thành phần này là bước quan trọng sống còn trong thiết kế, bởi chúng sẽ định hình sự phụ thuộc sau này.

Trong bài toán Giải phương trình bậc 1 (GPT1), các thực thể quan trọng nhất của chúng ta là:

– Các hằng số đầu vào (gồm cả a và b).

– Kết quả của phương trình: vô nghiệm, vô số nghiệm hoặc x.

Và quy trình quan trọng nhất của chúng ta là trả về một kết quả (với một trong 3 giá trị ở trên), quy trình này sẽ không thay đổi và không phụ thuộc vào việc bạn lấy hằng số đầu vào từ đâu hay kết quả sẽ được dùng để làm gì. Nhưng tất nhiên, quy trình này phụ thuộc vào các thực thể phía trên, một khi các thực thể này thay đổi, quy trình này cũng sẽ phải thay đổi.

Ta thống nhất với nhau gọi các thực thể này là các entity. Vậy là có 2 entity Constants (chứa a và b) và Result (chứa kết quả trả về).

Ta cũng gọi quy trình giải phương trình đã nói ở trên là một use case và thành phần xử lý quy trình này là Solver. (Bạn có thể đọc trên daohainam.com để tìm hiểu thêm về use case).

Vậy ta đã tách bài toán ra thành 3 bài toán nhỏ hơn:

– Làm sao để đọc vào được một Constants?

– Làm sao để giải bài toán với Solver?

– Làm sao để xuất dữ liệu từ Result?

Ba bài toán này hoàn toàn độc lập với nhau và chỉ phụ thuộc vào các entity.

Đọc dữ liệu vào Constants:

Trong chương trình đầu tiên, ta có Constants được đọc vào từ bàn phím thông qua System.Console. Tuy nhiên, đây là một phương thức nhập liệu cụ thể, do vậy chúng ta sẽ bị “dính cứng” và buộc phải sửa mã nguồn nếu muốn thay đổi. Vì vậy, bước đầu tiên là phải trừu tượng hóa nó, thay vì đọc từ bàn phím ta sẽ đọc từ một IConstantsReader, một IConstantsReader sẽ trả về một danh sách các Constants mà nó đọc được, bất kể chúng đến từ đâu. Sau này ta có thể implement ConsoleConstantsReader, FileConstantsReader, SqlServerConstantsReader hay thậm chí là VoiceConstantsReader. Việc thay đổi một phương thức nhập liệu mới sẽ vô cùng đơn giản, đơn giản tới mức nếu viết tốt ta thậm chí không cần biên dịch lại mã nguồn.

Xuất dữ liệu từ Result:

Tương tự với việc đọc dữ liệu, ta cũng sẽ thiết kế một IResultWriter.

Giải bài toán với Solver:

Ta sẽ có một ISolver nhận vào một dãy các Constants và trả về một dãy Result tương ứng.

Tại sao ta lại phải có một ISolver? Tại sao phải trừu tượng hóa? Chẳng phải cách giải phương trình bậc 1 đã quá đơn giản và cụ thể? Đơn giản đến mức ta không thể làm sai, đến mức ta không cần viết unit test? Và chắc chắn là quy trình giải bài toán PTB1 này cũng không bao giờ thay đổi!

Ở đây bạn cần cẩn thận, bạn có thể đang nhầm lẫn giữa phương pháp giải toán và cách chúng ta implement phương pháp giải toán đó! Phương pháp giải không bao giờ thay đổi nhưng cách implement hoàn toàn có thể thay đổi. Khi khách hàng Ả Rập yêu cầu giải một triệu phương trình trong 1 giờ bạn đã phải làm gì? Chia cho 5 máy tính và mỗi máy sẽ giải một phần của toàn bộ đầu vào. Ai sẽ là người chia? Chắc chắn trong kiến trúc của bạn sẽ phải xuất hiện thêm một thành phần đóng vai trò điều phối. Bạn nghĩ sao nếu tôi chỉ đơn giản là tạo một DistributedSolver (vẫn là một ISolver), nó sẽ không giải bài toán nào cả mà chỉ đơn thuần phân chia Constants và tập hợp lại kết quả, mô hình chương trình của bạn vẫn giữ nguyên, tôi vẫn dùng được các unit test của ISolver cho DistributedSolver. Có thể lúc viết chương trình lần đầu bạn chưa nghĩ đến điều này, có thể tôi cũng vậy, tuy nhiên vì tôi luôn tuân theo quy tắc “áp dụng tối đa trừu tượng hóa” nên khi gặp vấn đề tôi sẽ giải quyết dễ dàng hơn bạn nhiều.

Vậy là tới đây ta đã có các thành phần cơ bản và trừu tượng nhất, nhưng cũng là quan trọng nhất với chương trình này. Trong hình minh họa tôi đã vẽ 2 hình tròn, hình bên ngoài sẽ phụ thuộc vào bên trong, nhưng các thành phần ở trong không biết gì về sự tồn tại của các thành phần bên ngoài. Điều này đồng nghĩa với việc một thay đổi ở vòng ngoài sẽ không ảnh hưởng gì đến cái bên trong. Hình vẽ này rất quan trọng, chúng ta sẽ tiếp tục bổ sung thêm vào trong các bài viết sau.

Xem phần tiếp theo: https://daohainam.com/2023/10/24/open-close-principle-phan-cuoi/

2 thoughts on “Open – Close Principle – Phần 2

Leave a comment