5 nguyên tắc thiết kế được chia sẻ bởi chuyên gia về kiến trúc phần mềm

29 tháng 3, 2022 By DEVERA ACADEMY

Đâu là sự khác biệt giữa design principle và design pattern?

Chúng ta có thể hỏi Robert C. Martin, lập trình viên từ năm 1970 và là người tạo nguyên tắc SOLID. Ông ấy chính là một chuyên gia mà chúng ta có thể đề cập đến.

Bài viết này dựa trên tác phẩm xuất sắc của Robert “Clean Architecture: A Craftsman’s Guide to Software Structure and Design” (Prentice Hall), cuốn sách mà một người có đam mê ở chủ đề kiến ​​trúc phần mềm nên đọc. Nếu bạn quan tâm đến chủ đề này, bạn có thể bắt đầu với: 4 sự thật được chia sẻ bởi chuyên gia về kiến trúc phần mềm.

Hiểu một cách đơn giản design principle - “nguyên tắc thiết kế” là một hướng dẫn trừu tượng (không cụ thể cho bất kỳ ngôn ngữ lập trình nào) giúp bạn đưa ra các lựa chọn chính xác hơn ở bất kỳ cấp độ nào của kiến ​​trúc phần mềm.

Còn design pattern “mẫu thiết kế” là một kỹ thuật được thiết lập để giải quyết một vấn đề trong thế giới thực và chúng ta áp dụng nó khi xác định được các điều kiện cụ thể cần được đáp ứng. Một design pattern có thể được sinh ra từ một design principle.


Nguyên tắc SOLID

“Một hệ thống phần mềm tốt bắt đầu với clean code. Ngược lại, nếu các phần code nhỏ bên trong kém thì kiến ​​trúc phần mềm tốt cũng chẳng đóng góp được gì nhiều. Nhưng trong nhiều trường hợp, chúng ta hoàn toàn có thể kết hợp mọi thứ thành một mớ hỗn độn “xinh đẹp” ngay cả khi các khối gạch đã được làm rất tốt. Đây chính là lúc chúng ta cần đến nguyên tắc SOLID. "

SOLID là từ viết tắt của 5 nguyên tắc sau:

  • Single-responsibility principle (SRP)

  • Open–closed principle (OCP)

  • Liskov substitution principle (LSP)

  • Interface segregation principle (ISP)

  • Dependency Inversion principle (DIP)

Mục tiêu chung của các nguyên tắc này là tạo ra các cấu trúc phần mềm mức trung gian: có tính chịu lỗi, dễ hiểu và làm cơ sở cho các thành phần có thể được sử dụng trong nhiều hệ thống phần mềm.


Single-responsibility principle (SRP)

Bởi vì cái tên của nó, đây có lẽ là nguyên tắc khó hiểu nhất trong tất cả. SRP không phải là mỗi mô-đun chỉ nên có một trách nhiệm!

Thay vào đó, SRP chỉ ra rằng “một mô-đun nên có một và chỉ một lý do để thay đổi”. Vì những thay đổi trong phần mềm xuất phát từ yêu cầu của các tác nhân khác nhau, chúng ta có thể xây dựng nguyên tắc tốt hơn như:

“Một mô-đun phải chịu trách nhiệm cho một và chỉ một tác nhân”

Trong ví dụ sau, các hàm để tính toán tiền lương và giờ làm trong cùng một lớp và chia sẻ cùng một thuật toán (Giờ thông thường):


Vấn đề ở đây là nhóm giám đốc tài chính (CFO) và nhóm giám đốc điều hành (COO) là hai tác nhân khác nhau và nhu cầu và yêu cầu của họ có thể thay đổi vì những lý do khác nhau và trong những thời điểm khác nhau. Khi nhóm giám đốc tài chính thay đổi thuật toán cho mục đích riêng của họ, điều đó cũng có thể ảnh hưởng đến một số tính năng của nhóm COO với các lỗi tiềm ẩn và có thể xảy ra. Mô-đun này chịu trách nhiệm của quá nhiều tác nhân và phải thay đổi thường xuyên, tạo ra các hiệu ứng phụ và lỗi.

Giải pháp:

Có nhiều giải pháp, nhưng mỗi giải pháp sẽ di chuyển các chức năng đến các lớp khác nhau và độc lập:


EmployeeFacade chứa code để khởi tạo và ủy quyền công việc cho các lớp chuyên dụng chỉ có các chức năng (một cho mỗi tác nhân). Dữ liệu được đặt trong EmployeeData và tách biệt khỏi các hàm.

Lưu ý rằng nguyên tắc này không giải quyết sự cố kỹ thuật mà thay vào đó duy trì một số giá trị kiến ​​trúc như khả năng bảo trì.

Open–Closed Principle (OCP)

Nguyên tắc này giải thích lý do cơ bản tại sao chúng ta nghiên cứu kiến ​​trúc phần mềm:

"Các thực thể của phần mềm nên mở để mở rộng, nhưng đóng cửa để sửa đổi"

Vì vậy, trong trường hợp thêm hoặc mở rộng một tính năng mà không muốn sửa đổi nhiều mã hiện có, chúng ta muốn mở rộng một dựa trên code đã có với một cơ chế dễ dàng hơn. Nhưng làm thế nào để đạt được điều này?

Nguyên tắc SRP đã gợi ý chúng ta nên tách biệt các chức năng thay đổi vì những lý do khác nhau. Bây giờ OCP gợi ý cho chúng ta cách tổ chức các chức năng trong một hệ thống phân cấp của các mô-đun, để các mô-đun cấp cao hơn (nơi chứa các quy tắc hoạt động) được bảo vệ khỏi những thay đổi của các cấp phía dưới.

Trong hình sau, mũi tên A → B có nghĩa là A phụ thuộc / sử dụng B và đồng thời B không biết gì về A và được bảo vệ khỏi bất kỳ sự thay đổi nào của A.


LƯU Ý: mỗi mô-đun trên được cấu tạo bởi nhiều lớp / giao diện và chúng ta thường cần sử dụng nguyên tắc DIP (sử dụng các interfaces như đoạn bên dưới) để đảo ngược sự phụ thuộc nhằm đạt được phân cấp như mong muốn.

Tham khảo ví dụ minh họa, ngay cả khi Controller là ngoại vi của Interactor, nó vẫn là trung tâm của Presenter  và View. Và trong khi Presenters có thể là ngoại vi của Controller, họ vẫn là trung tâm của View. Lý do này đã dẫn chúng tôi đến định hướng như thể hiện trong hình.

Trong ví dụ này, bất kỳ thay đổi nào đối với các mô-đun cấp thấp sẽ không ảnh hưởng đến các mô-đun cấp cao. Vì vậy, theo định nghĩa OCP, bây giờ chúng ta có thể mở rộng hệ thống (ví dụ: với kiểu presenter mới) giảm các sửa đổi đối với code hiện có.


Liskov Substitution Principle (LSP)

Nguyên tắc ban đầu do Barbara Liskov đề xuất, đây là một nguyên tắc thiết kế rộng chỉ ra rằng một đối tượng (chẳng hạn như một lớp) và một đối tượng con (chẳng hạn như một lớp mở rộng lớp đầu tiên) có khả năng hoán đổi cho nhau mà không phá vỡ chương trình.

Với tư cách là một Software Architect, chúng ta nên nghĩ theo hướng rộng hơn. Nguyên tắc LSP có thể được áp dụng không chỉ trong trường hợp thông thường của một lớp mở rộng một lớp khác. Chúng ta có thể sử dụng nguyên tắc này mỗi khi chúng tôi có nhiều hành vi nằm sau một “interface”.

Interface có thể là một Java interface, các lớp Ruby chia sẻ cùng một  method signature, cũng có thể là một tập hợp các dịch vụ respond cùng một REST interface! Nói chung là khái niệm chung về interface.

Theo nghĩa đó, chúng ta cần có một hướng dẫn khi là một client sử dụng các hành vi (behaviour) được gói gọn trong “interface” này:

“Hành vi trên client mô-đun không được phụ thuộc vào các subtypes mà server mô-đun sử dụng”

Hãy xem một ví dụ về việc vi phạm nguyên tắc LSP để hiểu rõ hơn lý do tại sao LSP lại quan trọng:  User client tin rằng nó đang giao tiếp với Rectangle module ngay cả khi trong thời gian chạy nó đang là Square ( extends từ Rectangle), khiến cho việc tính diện tích không được thực hiện như mong đợi của client:


Vấn đề chính ở đây là việc sử dụng sai kế thừa đã vi phạm LSP. Để khắc phục vấn đề, chúng ta có thể bắt đầu xác định các cơ chế bổ sung trên server  để hiểu bản chất thực tế của hình dạng và đưa ra cơ chế phù hợp (if isSquare (r) then…). 

Giải pháp:

Chúng ta cần thiết kế lại sự tương tác của các mô-đun client-server tạo ra các quan hệ đa hình tuân theo nguyên tắc LSP, làm cho client độc lập với các kiểu con của server mô-đun.

Bằng cách này, code trong User sẽ không bao giờ phải cụ thể cho Square hoặc Rectangle để tính diện tích và sẽ không bị thay đổi và tăng độ phức tạp nếu chúng tôi giới thiệu các kiểu phụ mới (ví dụ: Circle).


Interface Segregation Principle (ISP)

Nguyên tắc này chỉ đơn giản gợi ý cho chúng ta rằng:

"Không có code nào bị buộc phải phụ thuộc vào những gì nó không sử dụng"

Trong ví dụ sau, User1 chỉ sử dụng phương thức “op1 ()” của giao diện OPS. Theo cách này, User1  sẽ vô tình phụ thuộc vào “op2 ()” và “op3 ()” mặc dù không sử dụng chúng:

Giải pháp:

Giải pháp cũng giải thích tên của nguyên tắc ISP. Các hoạt động được tách biệt thành các interface nhỏ hơn và cụ thể hơn để client chỉ biết về các methods liên quan tới nó.


Tuy nhiên, giải pháp này liên quan đến loại ngôn ngữ nên nếu suy nghĩ dưới góc độ của một Software Architect, chúng ta phải suy nghĩ về các hàm ý cao hơn để đưa ra hướng dẫn trong quá trình lựa chọn thiết kế.

Ví dụ, một architect chọn giới thiệu trong hệ thống S của mình một framework F, nó phụ thuộc vào cơ sở dữ liệu D cho một số tính năng cụ thể mà chúng ta không cần. Bây giờ Hệ thống S sẽ phụ thuộc vào những thay đổi và khiếm khuyết của cơ sở dữ liệu D theo một cách bắc cầu!

Luôn cố gắng giảm thiểu sự phụ thuộc vào những thứ không cần thiết.


Dependency Inversion Principle (DIP)

Như bạn có thể đã hiểu, tính linh hoạt là một giá trị thiết yếu trong kiến ​​trúc phần mềm.

Nguyên tắc DIP nói rằng:

"Tính linh hoạt tối đa của hệ thống đạt được khi các dependencies trong mã nguồn chỉ đề cập đến các phần trừu tượng"

Nghĩ đến Java, về lý thuyết, mọi hướng dẫn chỉ nên tham chiếu đến các interface, các lớp trừu tượng và thường không đề cập đến các lớp cụ thể. Rõ ràng là không thể và nó cũng không thực sự hữu ích: chẳng hạn mỗi khi chúng ta sử dụng String, chúng ta tạo ra một phụ thuộc vào một lớp cụ thể. Vậy chúng ta nên làm gì?

“Điều chúng ta thực sự quan tâm là KHÔNG phụ thuộc vào các lớp cụ thể không có tính ổn định và dễ thay đổi”

Lớp String rất ổn định và chúng ta không lo lắng gì về tính phụ thuộc ở đây cả. Ở đây đề cập đến các lớp cụ thể không ổn định, chúng ta có một loạt các kỹ thuật cho phép chúng ta giải quyết vấn đề, AbstractFactory là một trong số chúng:


Giải pháp:

Ứng dụng phụ thuộc vào một AbstractFactory (ổn định) để yêu cầu tạo một đối tượng cụ thể (dễ thay đổi) từ đó chúng ta sẽ không phụ thuộc trực tiếp vào nó vì những gì chúng ta sẽ nhận được về mặt tĩnh chỉ là Interface (ổn định) được thực hiện bởi đối tượng cụ thể.

Ok nhưng… tại sao nguyên tắc này được gọi là “đảo ngược phụ thuộc”?


Hãy chú ý vào đường cong được vẽ trong hình ở ví dụ ngay phía trên. Đường ong đó thực sự quan trọng vì nó là phân định của kiến trúc. Nó tách phần trừu tượng khỏi phần cụ thể và với chúng ta có thể quyết định nơi đặt các phân định để tổ chức hệ thống trong các mô-đun và xác định thứ bậc của các mô-đun (xem nguyên tắc OCP).


Chúng ta có thể sử dụng nguyên tắc DIP để đảo ngược sự phụ thuộc giữa hai package / mô-đun của chúng ta, nếu chúng ta nghĩ rằng sự phụ thuộc hiện tại hướng tới một package không ổn định, để định hướng sự phụ thuộc theo hướng mong muốn:

Kết luận

Kiến trúc phần mềm là một chủ đề phức tạp và hấp dẫn có nguồn gốc từ các nguyên tắc sinh ra từ kinh nghiệm. Các nguyên tắc SOLID thể hiện điểm khởi đầu tốt nhất để tạo ra một Clean Architecture.


Tác giả Luca Pelosi

Dịch bởi Devera Academy