Nguyên tắc SOLID trong lập trình Ruby
Bài đăng này đã không được cập nhật trong 7 năm
Trong quá trình làm việc với những ngôn ngữ hướng đối tượng, có thể bạn đã từng nghe qua về khái niệm design principles (những nguyên tắc thiết kế). Đây là tập hợp các hướng dẫn hỗ trợ lập trình viên đạt được mục tiêu viết code trong sáng, dễ đọc, dễ mở rộng và bảo trì. Nghe thì hấp dẫn dẫn như vậy, tuy nhiên việc đảm bảo mã nguồn tuân theo những nguyên tắc này là không dễ dàng và thi thoảng là không thể làm được. Do đó bạn nên tham khảo chúng như là những hướng dẫn, thay vì coi đó là những điều luật bắt buộc phải thực hiện.
Một trong những bộ nguyên tắc thiết kế hướng đối tượng nổi tiếng nhất là SOLID. Đây là từ được tạo nên bằng cách ghép các chữ cái đầu tiên của 5 nguyên tắc:
- Single responsibility principle - nguyên tắc đơn trách nhiệm
- Open/closed principle - nguyên tắc đóng/mở
- Liskov substitution principle - nguyên tắc thay thế Liskov
- Interface segregation principle - nguyên tắc phân tách interface
- Dependency inversion principle - nguyên tắc đảo ngược dependency
Sau đây, chúng ta sẽ xem xét từng nguyên tắc và cách sử dụng chúng để có thể gia tăng chất lượng của những dòng code Ruby. Đối với những ngôn ngữ lập trình hướng đối tượng khác, việc áp dụng tư tưởng của những nguyên tắc này là tương tự.
1. Single responsibility principle
A class should have only one reason to change. (xem thêm)
Trong hầu hết những tình huống lập trình, bạn được khuyến khích nên cố gắng tuân theo nguyên tắc này.
Hãy cùng quan sát ví dụ sau:
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 sử dụng cho việc xác thực người dùng, hay nói cách khác là kiểm tra xem email và mật khẩu họ nhập vào có khớp với dữ liệu được lưu trong database hay không. Nó có 2 nhiệm vụ (kiểm tra email, mật khẩu; xác thực người dùng), và theo nguyên tắc này thì nó chỉ nên giữ lại 1 nhiệm vụ.
Có thể tách đoạn code này thành 2 class, mỗi class thực hiện một trách nhiệm như sau:
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
class AuthenticatesUser
def authenticate email, password
if MatchesPasswords.new(email, password).matches?
do_some_authentication
else
raise NotAllowedError
end
end
end
Kỹ thuật extract class đã được áp dụng đối với class AuthenticatesUser
ban đầu. Kết quả là code được refactor, tuân thủ theo nguyên tắc đơn trách nhiệm.
2. Open/closed principle
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. (xem thêm)
Xét ví dụ dưới đây:
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 tắc đóng/mở bởi vì nếu bạn muốn thay đổi định dạng in ra của báo cáo, bạn cần thay đổi trực tiếp mã nguồn của class Report
. Chúng ta có thể refactor như sau:
class Report
def body
generate_reporty_stuff
end
def print formatter: JSONFormatter.new
formatter.format body
end
end
Với cách viết này, bạn có thể dễ dàng thay đổi định dạng in ra của báo cáo bằng cách truyền vào method print
tham số phù hợp:
report = Report.new
report.print formatter: XMLFormatter.new
Rõ ràng, chúng ta đã mở rộng chức năng của class mà không thay đổi code. Kỹ thuật được sử dụng ở đây là dependency injection. Tuy nhiên, bạn cũng có thể sử dụng những kỹ thuật khác để đạt được hiệu quả tương tự.
3. Liskov substitution principle
Barbara Liskov, giáo sư ngành khoa học máy tính tại học viện MIT nổi tiếng đã trình bày nguyên tắc này như sau:
If for each object o1 of type S there is an object o2 of type T such that for all programs P defined interms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T. (xem thêm)
Nguyên tắc này chỉ áp dụng cho tính chất kế thừa trong lập trình hướng đối tượng. Chúng ta sẽ tạo ra một class Cat
kế thừa class Animal
như sau:
class Animal
def walk
do_some_walkin
end
end
class Cat < Animal
def run
run_like_a_cat
end
end
Để tuân thủ theo nguyên tắc thay thế Liskov, chúng phải có cùng interface. Đối với ngôn ngữ Ruby, chúng ta có thể thực hiệ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
4. Interface segregation principle
Clients should not be forced to depend upon interfaces that they do not use. (xem thêm)
Chúng ta sẽ minh họa nguyên tắc này cụ thể hơn thông qua ví dụ một class có 2 client sử dụng nó:
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
Class Car
có một interface
được sử dụng một phần bởi cả 2 class Driver
và Mechanic
. Chúng ta có thể viết code tốt hơn 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
Bằng cách tách interface
ban đầu ra làm hai, chúng ta đã đảm bảo mã nguồn tuân thủ theo nguyên tắc phân tách interface.
5. Dependency inversion principle
- 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 abstraction. (xem thêm)
Chúng ta hãy cùng quay trở lại với ví dụ đưa ra ở mục 2, nguyên tắc đóng/mở, tuy nhiên sẽ có một chút thay đổi:
class Report
def body
generate_reporty_stuff
end
def print
JSONFormatter.new.format body
end
end
class JSONFormatter
def format body
...
end
end
Bây giờ chúng ta đã có một lớp để thực hiện việc định dạng in ra của báo cáo. Tuy nhiên, dễ thấy rằng code trong method print
của class Report
vẫn đang được fix cứng, từ đó sẽ tạo nên sự phụ thuộc của class này vào class JSONFormatter
. Vì Report
là class trừu tượng ở mức cao hơn so với JSONFormatter
, cách viết này đang vi phạm nguyên tắc đảo ngược dependency.
Có thể giải quyết vấn đề này tương tự như cách chúng ta đã thực hiện với nguyên tắc đóng/mở, bằng cách sử dụng kỹ thuật dependency injection:
class Report
def body
generate_reporty_stuff
end
def print formatter: JSONFormatter.new
formatter.format body
end
end
Sau khi được refactor, class Report
đã không phụ thuộc vào class JSONFormatter
và có thể sử dụng bất kỳ định dạng nào mà có định nghĩa method format
.
Lại một lần nữa, chúng ta thấy dependency injection được áp dụng để giải quyết vấn đề. Đây là một kỹ thuật khá hiệu quả khi mục đích của bạn là tách riêng các object.
6. Kết luận
Trên đây mình đã trình bày 5 nguyên tắc SOLID nổi tiếng trong thiết kế hướng đối tượng. Nếu chúng được áp dụng rộng rãi thì gần như sẽ cải thiện chất lượng code của bạn rất nhiều. Bên cạnh đó, có nhiều kỹ thuật khác nữa, và đôi khi việc vi phạm những nguyên tắc ấy lại...cần thiết hơn là tuân thủ theo. Điều quan trọng nhất ở đây chính là bạn hiểu biết, nắm vững càng nhiều những nguyên tắc thiết kế, bạn sẽ có kinh nghiệm phong phú hơn để có thể đưa ra quyết định hợp lý cho những tình huống cụ thể.
Chủ đề của bài viết này không dễ và những nội dung đã trình bày có thể khó tránh khỏi thiếu sót. Mình hi vọng sẽ nhận được những nhận xét, góp ý của các bạn để chúng ta cùng hiểu vấn đề tốt hơn, chính xác hơn. Cảm ơn các bạn đã đón đọc.
7. Tài liệu tham khảo
Sách:
- Robert C. Martin, Agile Software Development: Principles, Patterns, and Practices, Prentice Hall, 2002.
Bài viết internet:
- Doan Dai Nghia, SOLID Ruby: Single Responsibility Principle, 2017.
- Pham Van Duc, Sơ lược Object Oriented Design Principles, 2016.
- Tran Duc Thang, Object Oriented Design Principles, 2016.
- Ilija Eftimov, SOLID Principles in Ruby, 2014.
- Luis Zamith, SOLID Principles in Ruby, 2013.
All rights reserved