SOLID Principles in Ruby
SOLID Principle là những nguyên lý thiết kế OOP, được đúc kết từ rất nhiều kinh nghiệm của lập trình viên thông qua các dự án lớn nhỏ. Một project áp dụng những nguyên lý này sẽ có code dễ đọc, dễ test, rõ ràng hơn. Và việc quan trọng nhất là việc maintainace code sẽ dễ hơn rất nhiều. Nắm vững ...
SOLID Principle là những nguyên lý thiết kế OOP, được đúc kết từ rất nhiều kinh nghiệm của lập trình viên thông qua các dự án lớn nhỏ. Một project áp dụng những nguyên lý này sẽ có code dễ đọc, dễ test, rõ ràng hơn. Và việc quan trọng nhất là việc maintainace code sẽ dễ hơn rất nhiều.
Nắm vững về những nguyên lý này, đồng thời áp dụng chúng trong việc viêc code sẽ giúp bạn tiến thêm một bước trên con đường trở thành senior nhé.
SOLID bao gồm 5 nguyên lý dưới đây:
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
Ở bài viết này, mình xin giới thiệu một cách khái quát nhất về những nguyên lý này, đồng thời minh họa cách mà các bạn có thể sử dụng chúng trong ngôn ngữ Ruby.
Single responsibility principle
Một class chỉ nên giữ một trách nhiệm duy nhất (Chỉ có thể thay đổi class vì một lý do duy nhất)
Ta có thể tạm hiểu “trách nhiệm” ở đây tương đương với “chức năng”. Tại sao một class chỉ nên giữ một chức năng duy nhất.
Một class có quá nhiều chức năng cũng sẽ trở nên cồng kềnh và phức tạp. Trong ngành IT, requirement rất hay thay đổi, dẫn tới sự thay đổi code. Nếu một class có quá nhiều chức năng, quá cồng kềnh, việc thay đổi code sẽ rất khó khăn, mất nhiều thời gian, còn dễ gây ảnh hưởng tới các module đang hoạt động khác.
Để hiểu điều này, mình sẽ mình họa bằng một ví dụ.
class AuthenticatesUser def authenticate email, password if matches? email, password do_some_authentication else raise NotAllowedError end end private def matches? email, password user = find_from_db :user, @email user.encrypted_password == encrypt password end end
class AuthenticatesUser có nhiệm vụ xác nhận User khi email và password được match với dữ liệu trong Database. Nó đang làm 2 nhiệm vụ nhưng theo nguyên lý class này chỉ nên làm một nhiệm vụ mà thôi. Ta có thể sửa lại như sau:
class AuthenticatesUser def authenticate email, password if MatchesPasswords.new(email, password).matches? do_some_authentication else raise NotAllowedError end end end class MatchesPasswords def initialize email, password @email = email @password = password end def matches? user = find_from_db :user, @email user.encrypted_password == encrypt @password end end
Như vậy đã sinh ra 2 class AuthenticatesUser và MatchesPasswords để thực hiện 2 chức năng là xác nhận User và kiểm tra match email và password trong Database
Open/closed principle - Nguyên lý Đóng Mở
Có thể thoải mái mở rộng 1 module, nhưng hạn chế sửa đổi bên trong module đó (open for extension but closed for modification).
Theo nguyên lý này, một module cần đáp ứng 2 điều kiện sau:
- Dễ mở rộng: Có thể dễ dàng nâng cấp, mở rộng, thêm tính năng mới cho một module khi có yêu cầu.
- Khó sửa đổi: Hạn chế hoặc cấm việc sửa đổi source code của module sẵn có.
Hãy đến với một ví dụ:
class Report def body generate_reporty_stuff end def print body.to_json end end
Đoạn code trên vi phạm nguyên lý đóng mở, bởi vì khi chúng ta muốn thay đổi nội dụng được in ra của method print, ta phải thay đổi code của class. Bây giờ ta sẽ sửa lại đoạn code trên một chút
class Report def body generate_reporty_stuff end def print formatter: JSONFormatter.new formatter.format body end end
Thật dễ dàng để thay đổi nội dung được in ra của method print
report = Report.new report.print formatter: XMLFormatter.new
Mình vừa mở rộng một method mà không cần thay đổi code bên trong nó. Chắc nhiều bạn dev đã và đang sử dụng nguyên lý này mỗi ngày nhưng không biết tên của nó nhỉ.
Liskov Substitution Principle
Trong một chương trình, các object của class con có thể thay thế class cha mà không làm thay đổi tính đúng đắn của chương trình
Nguyên lý này được sử dụng cho tính chất kế thừa của OOP, Vì vậy mình sẽ lấy một ví dụ về hướng đối tượng để giải thích về nguyên lý này
class Animal def walk do_some_walkin end end class Cat < Animal def run run_like_a_cat end end
Theo nguyên lý, 2 class này phái có chung Interface. Nhưng Ruby không có abstract methods, chúng ta có thể sửa đoạn code trên như sau:
class Animal def walk do_some_walkin end def run raise NotImplementedError end end class Cat < Animal def run run_like_a_cat end end
Interface segregation principle
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ể
Trước khi đến với nguyên lý này, mình sẽ nhắc lại định nghĩa về Interface cho các bạn tiện theo dõi
Interface là một lớp rỗng chỉ chứa khai báo về tên phương thức không có khai báo về thuộc tính hay thứ gì khác và các phương thức này cũng là rỗng. Bởi vậy bất kỳ lớp nào sử dụng lớp interface đều phải định nghĩa các phương thức đã khai báo ở lớp interface.
Để thiết kế một hệ thống linh hoạt, dễ thay đổi, các module của hệ thống nên giao tiếp với nhau thông qua interface. Mỗi module sẽ gọi chức năng của module khác thông qua interface mà không cần quan tâm tới implementation bên dưới. Như đã nói ở trên, do interface chỉ chứa khai báo rỗng về method, khi một class implement một interface, class đó phải implement toàn bộ các method được khai báo trong interface đó.
Điều này tương đương với việc nếu ta tạo ra 1 interface bự (hơn 100 method chẳng hạn), mỗi class sẽ phải implement toàn bộ 100 method đó, kể những method không bao giờ sử dụng đến. Nếu áp dụng ISP, ta sẽ chia interface này ra thành nhiều interface nhỏ, các class chỉ cần implement những interface có chức năng mà chúng cần, không cần phải implement những chức năng thừa nữa.
Đây được coi là nguyên lý dễ hiểu nhất của SOLID, mình sẽ minh họa bằng một ví dụ:
class Car def open end def start_engine end def change_engine end end class Driver def drive @car.open @car.start_engine end end class Mechanic def do_stuff @car.change_engine end end
Như bạn thấy, Class Car là một Interface, khi class Mechanic gọi đến đối tượng của Car nó sẽ phải kế thừa các phương thức không cần thiết là open vàstart_engine, mình sẽ sửa lại như sau:
class Car def open end def start_engine end end class CarInternals def change_engine end end class Driver def drive @car.open @car.start_engine end end class Mechanic def do_stuff @car_internals.change_engine end end
Chúng ta sẽ chia nhỏ class Interface Car thành 2 phần, thực hiện những chức năng riêng để phù hợp với nguyên lý.
Dependency inversion principle
Các module cấp cao không nên phụ thuộc vào các module cấp thấp. Cả 2 nên phụ thuộc vào abstraction.
Interface (abstraction) không nên phụ thuộc vào chi tiết, mà ngược lại. (Các class giao tiếp với nhau thông qua interface, không phải thông qua implementation.)
Trong bài, mình hay dùng từ module. Trong thực tế, module này có thể là 1 project, 1 file dll, hoặc một service. Để dễ hiểu, chỉ trong bài viết này, các bạn hãy xem mỗi module là một class nhé.
Với cách code thông thường, các module cấp cao sẽ gọi các module cấp thấp. Module cấp cao sẽ phụ thuộc và module cấp thấp, điều đó tạo ra các dependency. Khi module cấp thấp thay đổi, module cấp cao phải thay đổi theo. Một thay đổi sẽ kéo theo hàng loạt thay đổi, giảm khả năng bảo trì của code.
Nếu tuân theo DIP, các module cấp thấp lẫn cấp cao đều phụ thuộc vào 1 interface không đổi. Ta có thể dễ dàng thay thế, sửa đổi module cấp thấp mà không ảnh hưởng gì tới module cấp cao.
Để hiểu nguyên lý này, mình sẽ lấy lại ví dụ của Open/closed principle phía trên.
class Report def body generate_reporty_stuff end def print JSONFormatter.new.format body end end class JSONFormatter def format body ... end end
Chúng ta đang có 1 class JSONFormatter, hơn nữa, ta cũng gọi class này trong report, do đó tạo ra một dependency từ class report phụ thuộc vào JSONFormatter. Report là một module cấp cao hơn JSONFormatter, điều này vi phạm DIP.
Bây giờ, mình sẽ sửa lại bằng cách sử dụng dependency injection:
class Report def body generate_reporty_stuff end def print formatter: JSONFormatter.new formatter.format body end end
Class Report không còn phụ thuộc vào JSONFormatter và bạn có thể sử dụng bất kì kiểu format nào khi gọi đến method format
Lời kết
Cảm ơn các bạn đã cùng mình hoàn thành bài viết SOLID Principles in Ruby này. Hi vọng qua đây, các bạn có thể thu được những kiến thức hữu ích và áp dụng chúng vào việc thiết kế/ viết code.
Như mình đã nói ở phần giới thiệu, các nguyên lý này chỉ là hướng dẫn, giúp cho code của bạn tốt hơn, sạch hơn, dễ bảo trì hơn. Tuy nhiên, “không có bữa ăn trưa nào miễn phí”. Áp dụng các nguyên lý này vào code có thể giúp bạn cải thiện được chất lượng code, nhưng cũng có thể làm nó rườm rà, dài hơn, khó quản lý hơn.
Để thành một developer giỏi, ta nên biết các nguyên lý SOLID, các design patterns. Tuy nhiên, không phải cứ cứng nhắc áp dụng nhiều nguyên lý và design pattern vào code thì code sẽ tốt hơn. Một người developer giỏi sẽ hiểu rõ những trade-off của chúng và chỉ áp dụng một cách hợp lý để giải quyết vấn đề. Hãy nhớ, trong design, tất cả đều là đánh đổi nhé!
Tham khảo
https://toidicodedao.com/tag/series-solid/ https://subvisual.co/blog/posts/19-solid-principles-in-ruby