Tổng quan
SOLID là viết tắt của 5 chữ cái đầu trong 5 nguyên tắc thiết kế hướng đối tượng. Giúp cho lập trình viên viết ra những đoạn code dễ đọc, dễ hiểu, dễ maintain. Nó được đưa ra bởi Robert C. Martin và Michael Feathers. 5 nguyên tắc đó bao gồm:
-
Single responsibility priciple (SRP)
-
Open/Closed principle (OCP)
-
Liskov substitution principe (LSP)
-
Interface segregation principle (ISP)
-
Dependency inversion principle (DIP)
Các nguyên tắc
The Single Responsibility Principle (SRP)
There should never be more than one reason for a class to change. In other words, every class should have only one responsibility.
Nguyên lý này nói rằng mỗi lớp chỉ nên chịu một trách nhiệm (chịu 1 công việc) cụ thể nào đó mà thôi.
Do đó mỗi lần chúng ta tạo/sửa một class. Phải luôn tự hỏi trong lớp này đã đảm nhiệm bao nhiêu vai trò rồi?
Hãy xem ví dụ sau:
|
|
Sau khi mọi người đọc đoạn code trên, thì mọi người thấy class trên chịu trách nhiệm làm bao nhiêu công việc?
Class Handler chịu trách nhiệm lấy data từ API (1), parse data sang kiểu mảng string (2) và lưu data vào database (3). Áp dụng vào dự án thực tế, chúng ta dùng Alamofire để call API (1), ObjectMapper để parse data (2) và dùng Core Data để lưu data vào database (3). Đến lúc đó, code ở ví dụ sẽ trở nên cồng kềnh, khó bảo trì vào mở rộng.
Cách xử lý:
|
|
|
|
Nguyên lý này giúp class của bạn clean nhất có thể. Ngoài ra, ở ví dụ đầu tiên, bạn không thể test được requestDataToAPI
, parse and saveToDB một cách trực tiếp, vì nó là các private methods. Sau khi sửa lại code, chúng ra có thể dễ dàng testing các hàm này.
The Open-Closed Principle (OCP)
Software entities … should be open for extension, but closed for modification.
Nguyên lý nói rằng chúng ta không nên sửa đổi class có sẵn, chỉ nên mở rộng nó.
Nếu bạn muốn tạo một lớp dễ bảo trì, thì phải có 2 điều kiện quan trọng sau đây:
-
Open for extension: Có thể dễ dàng thêm và thay đổi hành vi (behaviours) của class đó 1 cách dễ dàng.
-
Close for modification: Không được thay đổi những hành vi đã có sẵn của class.
Chúng ta có ví dụ sau, class Logger có nhiệm vụ in ra chi tiết những lớp Car
|
|
Vậy nếu chúng ta muốn class Logger in thêm chi tiết của lớp mới khác, thì chúng ta phải thay đổi hàm printData()
mỗi lần như vậy. Điều này vi phạm nguyên lý OCP mà chúng ta đang giới thiệu.
Ví dụ mình sẽ thêm 1 class Bicycle. Mọi người sẽ thấy mình phải thay đổi lại hàm printData()
của class Logger
|
|
Cách giải quyết: Chúng ta sẽ tạo ra 1 protocol Printable, những class phương tiện (Car, Bicycle) sẽ conform protocol này. Mà hàm printData()
sẽ in ra 1 mảng của Printable
Bằng cách đó, chúng ta đã tạo thêm 1 lớp trừu tượng nằm giữa printData()
và lớp cần in dữ liệu. Cho phép thêm những lớp mới (ví dụ như Bicycle) mà không cần phải thay đổi code trong hàm printData()
|
|
The Liskov Substitution Principle (LSP)
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it
Nguyên lý này nói rằng: nếu class A là class con của class B. Thì những hàm trong class A phải thực hiện những hành động giống với class B. Để hiểu hơn nguyên lý này. Chúng ta hãy xem ví dụ sau:
|
|
Chúng ta có class Rectangle và 1 hàm tính diện tích (chiều dài nhân chiều rộng), class Square vì là hình vuông nên chúng ta có chiều dài bằng chiều rộng.
Chúng ta hãy xem cách tính diện tích của class Square:
|
|
Theo nguyên lý LSP, vì class Square kế thừa từ class Rectangle. Nên hàm area() phải luôn có giá trị bằng chiều dài nhân với chiều rộng (ở đây là 7x5 = 35). Tuy nhiên chúng ta lại nhận được diện tích là 25. Do đó, ví dụ trên đã vi phạm nguyên lý LSP này.
Cách giải quyết: Chúng ta sử dụng protocol Geometrics chứa hàm tính diện tích. Vậy class Square không còn kế thừa từ class Rectangle nữa. Mà cả 2 class này kế thừa từ protocol.
|
|
The Interface Segregation Principle (ISP)
Many client-specific interfaces are better than one general-purpose interface.
Nguyên lý này giới thiệu 1 trong những vấn đề của lập trình hướng đối tượng: interface quá to (the fat interface). Interface quá to, nghĩa là trong interface đó (ở trong lập trình iOS có thể hiểu interface là protocol) có quá nhiều hàm/thuộc tính. Nó chứa nhiều thông tin không cần thiết. Hãy xem ví dụ sau:
Chúng ta có protocol GestureProtocol với hàm didTap()
|
|
Sau 1 thời gian chúng ta làm việc, thì protocol này phình to ra. Như sau:
|
|
Class SuperButton của chúng ta cần cả 3 hàm trên, nên nó sẽ đúng khi chúng ta inform (kế thừa) SuperButton với GestureProtocol
|
|
Tuy nhiên, bắt đầu phát sinh 1 vấn đề khác là: chúng ta có 1 class PoorButton. Và class này chỉ cần duy nhất 1 hàm didTap()
|
|
Vậy là class PoorButton này bỏ trống 2 hàm didDoubleTap() và didLongPress(), nghĩa là chúng ta đã truyền bị dư 2 hàm không sử dụng cho class PoorButton. Do đó đã vi phạm nguyên lý ISP chúng ta đang đề cập ở đây.
Cách giải quyết: Đưa những hàm này ra thành các protocol riêng lẻ. Và chỉ truyền những protocol cần thiết.
|
|
The Dependency Inversion Principle (DIP)
High level modules should not depend upon low level modules. both should depend upon abstractions.
Abstractions should not depend upon details. details should depend upon abstractions.
Nguyên lý này khá quan trọng đối với lập trình viên. Và nó cũng liên quan đến Dependency Injection (DJ). Vậy Dependency Inversion (DI) là gì? Và DJ là gì? Giữa DI và DJ có mối quan hệ như thế nào? Mình sẽ giới thiệu ở bài blog sau.
Kết luận
Nếu bạn làm theo nguyên lý SOLID, bạn có thể tăng chất lượng code của mình. Code của chúng ta sẽ trở nên dễ đọc, dễ bảo trì và dễ mở rộng hơn cho sau này. Tuy nhiên, để áp dụng nguyên lý SOLID vào dự án thực tế sẽ có nhiều khó khăn hơn. Nhưng vì thế, chúng ta càng phải nên nắm vững kiến thức nền tảng này để có thể áp dụng và sửa đổi khi làm dự án thực tế nhé.