SOLID Principles in Ruby
Bài đăng này đã không được cập nhật trong 5 năm
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ủamodule
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
All rights reserved