SOLID Principles

Nếu là một developer, chắc các bạn đều đã nghe tới một số khái niệm trong OOP cơ bản như sau:

  • Abstraction (Tính trừu tượng)
  • Encapsulation (Tính bao đóng)
  • Inheritance (Tính kế thừa)
  • Polymophirsm (Tính đa hình)

Nhưng hôm nay mình sẽ không đề cập đến những tính chất trên mà mình sẽ giới thiệu đến mọi người tới chủ đề: Những nguyên tắc thiết kế trong OOP. Đây được coi là những nguyên lý được đúc kết bởi kinh nghiệm sương máu của vô số developer.

Chắc tới đây các bạn cũng đã nghĩ tới nguyên tắc SOLID rồi phải không?

SOLID là từ viết tắt của 5 nguyên tắc thiết kế hướng đối tượng(OOP). Các nguyên tắc này định hình ra một quy chuẩn cho việc phép triển phần mềm với các cân nhắc để duy trì và mở rộng khi phát triển dự án.

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

  • S - Single-responsiblity Principle
  • O - Open-closed Principle
  • L - Liskov Substitution Principle
  • I - Interface Segregation Principle
  • D - Dependency Inversion Principle

Trong bài biết này mình sẽ giới thiệu tới các bạn từng nguyên tắc một để các bạn có một cái nhìn thật tổng quan và chi tiết nhất.

Single-Responsibility Principle

Nguyên tắc này được phát biểu như sau: Một class nên có một và chỉ một lý do để thay đổi, nghĩa là một class chỉ làm một công việc duy nhất.

Ví dụ: chúng ta có một ứng dụng lấy một tập hợp các hình - hình tròn và hình vuông - và tính tổng diện tích của tất cả các hình đó.

Đối với hình vuông bạn sẽ cần biết được chiều dài của một cạnh:

Đối với hình tròn, bạn sẽ cần biết được bán kính của nó:

Tiếp theo, tạo class AreaCalculator và sau đó viết logic để tính diện tích của các hình được cung cấp:

Tiếp theo là sử dụng, bạn sẽ cần khởi tạo class AreaCalculator và truyền vào một mảng các hình dạng sau đó gọi là output() để hiển thị kết quả:

Tất cả logic đang được xử lý bởi class AreaCalculator . Điều này đã vi phạm nguyên tắc Single-Responsibility, class AreaCalculator chỉ nên chứa logic tính tổng diện tích của các hình và nó không nên biết đến định dạng kết quả đầu ra là gì.

Để giải quyết vấn đề này, bạn có thể tạo một class SumCalculatorOutputter để chuyên xử lý logic bạn cần để xuất dữ liệu cho người dùng:

Và sau đó việc sử dụng class SumCalculatorOutputter  sẽ như sau:

Bây giờ, logic bạn cần để xuất dữ liệu cho người dùng được xử lý bởi class SumCalculatorOutputter . Điều này đã thỏa mãn nguyên tắc Single-Responsibility

Open-Closed Principle

Nguyên tắc này được phát biểu như sau: Các đối tượng hoặc thực thể phải được mở để mở rộng nhưng bị đóng để sửa đổi.

Đ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 chính class đó. Mở rộng ở đây được hiểu là một class extends một class khác, hoặc là tái sử dụng chức năng tồn tại bên trong một class.

Tiếp tục với ví dụ trên, nhưng bạn hãy tập trung vào phương thức sum(). Hãy xem xét một trường hợp nếu người dùng muốn thêm các hình như tam giác, v.v. Bạn sẽ phải đi sửa đổi lại function này, cụ thể là bổ sung thêm khối if/else. Và điều này đã vi phạm nguyên tắc Open-Closed.

Cách giải quyết vấn đề này là bạn loại bỏ logic tính toán diện tích của mỗi hình học ra khỏi phương thức sum() và giao nó cho class của từng hình học quản lý.

Đây là phương thức area() được định nghĩa bên trong Square:

Và đây là phương thức area() được định nghĩa trong Circle:

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

Bây giờ, bạn có thể tạo một class hình dạng khác và chuyển nó vào khi tính tổng mà không vi phạm nguyên tắc Open-Closed.

Tuy nhiên có một vấn để khác lại nảy sinh là làm thế nào để bạn biết rằng đối tượng được truyền vào class AreaCalculator thực sự là một hình và bên trong class của hình đó đã có phương thức area() ?

Câu trả lời ở đây chính là Interface, Interface là một phần không thể thiếu trong SOLID.

Tại đây bạn tạo một function area() bên trong một interface là ShapeInterface.

Tiếp theo bạn sửa đổi lại các class để implement interface ShapeInterface:

Đây là bản cập nhật cho Square:

Và đây là bản cập nhật cho Circle:

Và trong phương thức sum(), bạn có thể kiểm tra xem các hình dạng được cung cấp có thực sự là instances của ShapeInterface.

Liskov Substitution Principle

Nguyên tắc này được phát biểu như sau: Gọi q(x) là một thuộc tính có thể cho phép đối với các đối tượng của x thuộc loại T. Khi đó q(y) sẽ có thể cho phép đối với các đối tượng y thuộc loại S trong đó S là một kiểu con của 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ó thể thay thế cho lớp cơ sở hoặc lớp cha của chúng.

Hãy xem một ví dụ về class VolumeCalculator mở rộng class AreaCalculator.

Class SumCalculatorOutputter đã được định nghĩa ở trên với nội dung như sau:

Nếu bạn cố gắng chạy một ví dụ như thế 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 về việc chuyển đổi mảng thành chuỗi.Để khắc phục điều này, thay vì trả vì một mảng ở phương thức sum() trong class VolumeCalculator chúng ta hãy trả về $summedData:

Interface Segregation Principle

Nguyên tắc này được phát biểu như sau: Thay vì dùng 1 interface lớn, ta nên tách thành nhiều interface nhỏ, với nhiều mục đích cụ thể.

Vẫn là ShapeInterface từ ví dụ trước,  ở đây gỉa sử ứng dụng của bạn cần phải bổ sung thêm các hình khối và hình cầu. Và các hình dạng này sẽ cần phải tính toán thể tích.

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ó khối lượng, vì vậy với interface này nó sẽ buộc class Square thực hiện 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 Interface Segregation. Thay vào đó, bạn có thể tạo ra một interface khác được gọi là ThreeDimensionalShapeInterfacecó function volume() và các hình dạng ba chiều có thể implement interface này.

Đây là một cách tiếp cận tốt hơn nhiều, nhưng có một vấn đề cần chú ý khi sử dụng các interface này. Thay vì sử dụng một ShapeInterfacehoặc một ThreeDimensionalShapeInterface, bạn có thể tạo ra một interface khác như ManageShapeInterface và triển khai nó trên cả ba hình phẳng và hình dạng ba chiều.

Bằng cách này bạn sẽ có một interface duy nhất để quản lý các hình dạng:

Bây giờ trong class AreaCalculator thay vì gọi phương thức area() bạn sẽ gọi tới phương thức calculate() và bạn có thể kiểm tra xem đối tượng hình học đó có phải là một instance của ManageShapeInterface hoặc ShapeInterface hay không.

Điều này sẽ thỏa mãn được nguyên tắc Interface Segregation

Dependency Inversion Principle

Nguyên tắc này được phát biểu như sau:  Các module cấp cao không nên phụ thuộc vào module cấp thấp mà cả hai nên phụ thuộc vào abstract.

Đây là một ví dụ về việc kết nối CSDL MySQL:

Đầu tiên, MySQLConnection là module cấp thấp và PasswordReminder là module cấp cao. Nhưng dựa trên phát biểu trên thì đoạn code này đang vi phạm nguyên tắc này vì class PasswordReminder đang phụ thuộc vào class MySQLConnection.

Sau này, nếu bạn có sự thay đổi về cơ sở dữ liệu, bạn cũng sẽ phải chỉnh sửa class PasswordReminder, điều này sẽ vi phạm nguyên tắc Open-Closed.

Các class  PasswordReminder không nên quan tâm tới cơ sở dữ liệu được sử dụng trong ứng dụng của bạn. Để giải quyết vấn đề này các bạn tạo thêm 1 interface.

Trong interface này có một phương thức connect() và class MySQLConnection sẽ implement interface này. Ngoài ra thay vì sử dụng trực tiếp MySQLConnection bên trong constructor của class PasswordReminder, thay vào đó bạn sẽ sử dụng DBConnectionInterface . Và khi có bất kỳ sự thay đổi nào về cơ sở dữ liệu thì điều bạn cần làm là thay đổi implement cho DBConnectionInterface.

Phần kết luận

Trong bài viết này mình đã trình bày 5 nguyên tắc trong SOLID. Hy vọng các bạn có thể hiểu và áp dụng trong các dự án thực tế.