SOLID: 5 nguyên tắc cơ bản trong kiến trúc hướng đối tượng
Giới thiệu
SOLID là từ viết tắt của 5 tính chất trong thiết kế hướng đối tượng (OOD), do Robert C.Martin đề. 5 tính chất này giúp cho việc phát triển phần mềm dễ dàng bảo trì, mở rộng sau này.
SOLID = single-responsibility, Open-closed, Liskov, Interface segregation, Dependency Inversion. Mình sẽ giải thích cụ thể từng tính chất và mình họa bằng ngôn ngữ PHP cho các bạn dễ hiểu nhé.
Single-Responsibility Principle (Duy nhất một nhiệm vụ)
Một class nên có một nhiệm vụ duy nhất, và chỉ thay đổi code bên trong class đó khi nhiệm vụ của nó bị thay đổi.
Mình sẽ lấy ví dụ làm về một ứng dụng tính tổng diện tích các hình học như hình tròn, hình vuông.
Việc đầu tiên là tạo các class Shape và có một vài params bắt buộc phải điền trong hàm khởi tạo.
Class cho hình vuông sẽ như sau:

Class cho hình tròn sẽ như sau:

Tiếp theo, chúng ta tạo một class AreaCalculator để tính tổng tất cả các diện tích của các khối được truyền vào. Diện tích của hình vuông là bình phương cạnh, còn của hình tròn là PI nhân với bình phương của bán kính.

Để sử dụng classs AreaCalculator này, thì mình sẽ khởi tạo một vài hình tròn, hình vuông và truyền chính vào class AreaCalculator để lấy được output của nó là gì.
Ví dụ bên dưới sẽ là:
- 1 hình tròn với bán kính là 2
- 1 hình vuông có cạnh là 5
- 1 hình vuông có cạnh là 6

Kết quả của chương trình trên là một string, vì phương thức output của class AreaCalculator trả về một string.
Vấn đề ở đây là AreaCalculator đang quy định kiểu dữ liệu trả về, nếu chúng ta muốn convert kết quả sang dạng khác thì sao? JSON chẳng hạn. Điều này không đúng với tính chất Single-responsibility, class AreaCalculator chỉ được nên dùng để tính tổng diện tích các khối được truyền vào.
Để giải quyết vấn đề trên, bạn có thể tạo một class riêng biệt phục vụ cho việc xử lý output. Ở đây, tạm thời gọi là SumCalculatorOutputter

Và thuật toán bây giờ sẽ trở thành như sau:

Đến thời điểm này thì toàn bộ logic liên quan đến output sẽ được thực hiện bởi class SumCalculatorOutputter. Điều này sẽ giúp cho code của chúng ta thõa mãn điều tính chất single-responsibility.
Open-Closed Principle (Tính chất đóng mở)
Objects or entities should be open for extension but closed for modification.
Điều này có nghĩa là một class có thể được mở rộng mà không cần sửa đổi logic bên trong nó.
Qua lại với class AreaCalculator và chú ý đến phương thức sum

Hãy xem xét một kịch bản trong đó người dùng muốn có tổng các hình dạng bổ sung như hình tam giác, hình ngũ giác, hình lục giác, v.v. Bạn sẽ phải liên tục chỉnh sửa tệp này và thêm nhiều khối if/else khác. Điều đó sẽ vi phạm nguyên tắc đóng mở.
Một cách bạn có thể làm cho phương thức tính tổng này tốt hơn là loại bỏ logic để tính diện tích của từng hình dạng ra khỏi phương thức lớp AreaCalculator và gắn nó vào lớp của từng hình dạng.
Đây là phương thức diện tích được định nghĩa trong Square:

Đây là phương thức diện tích được định nghĩa trong Circle:

Phương thức sum trong AreaCalculator sẽ được viết lại như sau:

Bây giờ, bạn có thể tạo một lớp hình dạng khác và chuyển nó vào khi tính tổng mà không vi phạm mã.
Tuy nhiên, một vấn đề khác phát sinh. Làm thế nào để bạn biết rằng đối tượng được truyền vào AreaCalculator thức sự là một class có chứa method area?
Interface là một phần không thể thiếu trong kiến trúc của SOLID, bạn hãy tạo một Interface chứa phương thức area.
Tạo một ShapeInterface có method area:

Sau đó, các shape class sẽ implements ShapeInterface này.


Chúng ta cần thay đổi một chút ở phương thức sum trong class AreaCalculator, chúng ta cần kiểm tra liệu một đối tượng truyền vào có phải là ShapeInterface hay không.

Và bây giờ, code của chúng ta thỏa mãn được tính đóng mở.
Liskov Substitution Principle
Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
Điều này có nghĩa là mọi lớp con hoặc lớp dẫn xuất phải được thay thế cho lớp cơ sở hoặc lớp cha của chúng. Xây dựng lớp AreaCalculator ví dụ, xem xét một lớp VolumeCalculator mới mở rộng lớp AreaCalculator:

Quay lại với class SumCalculatorOutputter vẫn đang như thế này:

Thử chạy chương trình này:

Khi bạn gọi phương thức HTML trên đối tượng $output2, bạn sẽ gặp lỗi E_NOTICE thông báo cho bạn về việc chuyển đổi một mảng thành chuỗi.
Để khắc phục điều này, thay vì trả về một mảng từ phương thức tính tổng của lớp VolumeCalculator, hãy trả về $summedData:

$summedData có thể là số float, double hoặc integer.
Điều đó thỏa mãn nguyên tắc thay thế Liskov.
Interface Segregation Principle
A client should never be forced to implement an interface that it doesn’t use, or clients shouldn’t be forced to depend on methods they do not use.
Vẫn xây dựng từ ví dụ ShapeInterface trước đó, bạn sẽ cần hỗ trợ các hình dạng ba chiều mới của Hình khối và Hình cầu, đồng thời những hình dạng này cũng sẽ cần tính toán thể tích.
Hãy xem xét điều gì sẽ xảy ra nếu bạn sửa đổi ShapeInterface để thêm một contract khác:

Bây giờ, bất kỳ hình dạng nào bạn tạo đều phải triển khai phương thức volume, nhưng bạn biết rằng hình vuông là hình phẳng và chúng không có thể tích, vì vậy Interface này sẽ buộc lớp Square triển khai một phương thức mà nó không sử dụng.
Điều này sẽ vi phạm nguyên tắc phân biệt interface. Thay vào đó, bạn có thể tạo một Interface khác gọi là ThreeDimensionalShapeInterface có volume contract và các hình dạng ba chiều có thể triển khai giao diện này:

Đây là một cách tiếp cận tốt hơn nhiều, nhưng một cạm bẫy cần chú ý là khi gõ gợi ý các Interface này. Thay vì sử dụng ShapeInterface hoặc ThreeDimensionalShapeInterface, bạn có thể tạo một Interface khác, có thể là ManageShapeInterface và triển khai Interface đó trên cả hình phẳng và hình ba chiều.
Bằng cách này, bạn có thể có một API duy nhất để quản lý các hình dạng:

Bây giờ trong lớp AreaCalculator, bạn có thể thay thế lệnh gọi phương thức area bằng calculate và cũng kiểm tra xem đối tượng có phải là một thể hiện của ManageShapeInterface chứ không phải ShapeInterface hay không.
Điều đó thỏa mãn nguyên tắc phân biệt Interface.
Dependency Inversion Principle
Entities must depend on abstractions, not on concretions. It states that the high-level module must not depend on the low-level module, but they should depend on abstractions.
Nguyên tắc này cho phép tách rời.
Dưới đây là ví dụ về PasswordReminder kết nối với cơ sở dữ liệu MySQL:

Đầu tiên, MySQLConnection là module cấp thấp trong khi PasswordReminder là cấp cao, nhưng theo định nghĩa của D trong SOLID, trạng thái này phụ thuộc vào sự trừu tượng hóa, không phụ thuộc vào sự cụ thể hóa. Đoạn mã trên vi phạm nguyên tắc này vì lớp PasswordReminder buộc phải phụ thuộc vào lớp MySQLConnection.
Sau này, nếu bạn thay đổi công cụ cơ sở dữ liệu, bạn cũng sẽ phải chỉnh sửa lớp PasswordReminder và điều này sẽ vi phạm nguyên tắc đóng-mở.
Lớp PasswordReminder không cần quan tâm ứng dụng của bạn sử dụng cơ sở dữ liệu nào. Để giải quyết những vấn đề này, bạn có thể viết mã cho một giao diện vì các module cấp cao và cấp thấp sẽ phụ thuộc vào tính trừu tượng:

Interface có một phương thức kết nối và lớp MySQLConnection thực hiện Interface này. Ngoài ra, thay vì trực tiếp gõ gợi ý lớp MySQLConnection trong hàm tạo của PasswordReminder, thay vào đó, bạn gõ gợi ý DBConnectionInterface và bất kể loại cơ sở dữ liệu mà ứng dụng của bạn sử dụng, lớp PasswordReminder có thể kết nối với cơ sở dữ liệu mà không gặp bất kỳ sự cố nào và mở-đóng nguyên tắc không được vi phạm.

Mã này thiết lập rằng cả mô-đun cấp cao và cấp thấp đều phụ thuộc vào sự trừu tượng hóa.
Tổng kết
Trong bài viết này, bạn đã được trình bày với năm nguyên tắc của SOLID Code. Các dự án tuân thủ các nguyên tắc SOLID có thể được chia sẻ với các cộng tác viên, được mở rộng, sửa đổi, thử nghiệm và tái cấu trúc với ít phức tạp hơn.