Áp dụng các nguyên lý SOLID trong thiết kế
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 developer viết ra những đoạn code dễ đọc, dễ hiểu, dễ maintain, được đưa ra bởi Bob Martin và Michael Feathers. Việc theo sát 5 nguyên tắc này nói thì để đáp ứng cả 5 nguyên tắc e là điều không đơn giản. 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) Trong bài viết này mình sẽ giới thiệu từng nguyên tắc trong 5 nguyên tắc trên cũng như cách áp dụng nó làm tăng chất lượng code trong Ruby
Single responsibility priciple
Nguyên lý đầu tiên ứng với chữ S
trong SOLID
, có ý nghĩa là một class chỉ nên giữ một trách nhiệm duy nhất. Một class có quá nhiều chức năng sẽ trở nên cồng kềnh và trở nên khó đọc, khó maintain. Mà đối với ngành IT việc requirement thay đổi, cần thêm sửa chức năng là rất bình thường, nên việc code trong sáng, dễ đọc dễ hiểu là rất cần thiết. Để hiểu rõ hơn, ta cũng soi vào đoạn code vi phạm nguyên tắc này:
class DealProcessor
def initialize(deals)
@deals = deals
end
def process
@deals.each do |deal|
Commission.create(deal: deal, amount: calculate_commission)
mark_deal_processed
end
end
private
def mark_deal_processed
@deal.processed = true
@deal.save!
end
def calculate_commission
@deal.dollar_amount * 0.05
end
end
Class này ngoài việc đánh dấu giao dịch đã được xử lý, còn thực hiện tính hoa hồng cho mỗi giao dịch nữa. Ngoài ra sau này còn có thể thêm chức năng ví dụ như là gửi mail nội dung cụ thể về hoa hồng cho người có liên quan, gửi thông báo v.v.. dẫn đến việc class này làm nhiều hơn 1 nhiệm vụ và đã vi phạm nguyên tắc đơn trách nhiệm trong SOLID. Có thể refactor lại như sau:
class DealProcessor
def initialize(deals)
@deals = deals
end
def process
@deals.each do |deal|
mark_deal_processed
CommissionCalculator.new.create_commission(deal)
end
end
private
def mark_deal_processed
@deal.processed = true
@deal.save!
end
end
class CommissionCalculator
def create_commission(deal)
Commission.create(deal: deal, amount: deal.dollar_amount * 0.05)
end
end
Giờ thì chúng ta đã có 2 class nhỏ hơn thực hiện 2 nhiệm vụ riêng biệt: một processor chiệu trách nhiệm lưu lại việc xử lý và 1 calculator chịu trách nhiệm tính toán và tạo ra các dữ liệu liên quan tới tiền hoa hồng.
Open/Closed principle
Nguyên lý thứ 2 ứng với chữ O
trong SOLID
. Nội dung Có thể thoải mái mở rộng 1 class nhưng không được sửa đổi bên trong class đó (open for extension but closed for modification)
Ta có đoạn code sau:
class Report
def body
generate_reporty_stuff
end
def print
body.to_json
end
end
Đoạn code trên vi phạm OCP bởi nếu ta muốn thay đổi định dạng của report được print ra, ta sẽ cần phải sửa đổi code của class. Refactor lại như sau:
class Report
def body
generate_reporty_stuff
end
def print(formatter: JSONFormatter.new)
formatter.format body
end
end
Làm theo cách này thì ta vừa mở rộng tính năng mà khi thay đổi format sẽ không cần phải thay đổi code
report = Report.new
report.print(formatter: XMLFormatter.new)
Liskov substitution principle
Nguyên tắc thứ 3, ứng với chữ L
trong SOLID
. Nội dung nguyên tắc này được phát biểu như sau: Bất cứ instance nào của class cha cũng có thể được thay thế bởi instance của class con của nó mà không làm thay đổi tính đúng đắn của chương trình
Hãy cùng xem ví dụ về việc vi phạm nguyên tắc Liskov dưới đây
class Rectangle
def set_height(height)
@height = height
end
def set_width(width)
@width = width
end
end
class Square < Rectangle
def set_height(height)
super(height)
@width = height
end
def set_width(width)
super(width)
@height = width
end
end
Hình vuông (Square
) là con của hình chữ nhật (Rectangle
), vì vậy để hợp lý hóa, khi ta gọi method set_height
hay set_width
của Square
thì đều phải modify cả @width và @height. Ở đây việc ứng dụng tính đa hình chưa đúng có thể gây hiểu lầm, ví dụ trong trường hợp iterate qua 1 collection của Rectangle
, mà trong đó lại có 1 instance của class Square
, việc gọi method set_height
nhưng lại gây ra hậu quả không mong muốn là thay đổi cả width
. Một ví dụ điển hình của việc vi phạm nguyên tắc Liskov đó là throw exception trong overridden method ở class con.
Interface segregation principle
Nguyên tắc thứ 4 ứng với chữ I
trong SOLID
, nội dung nguyên tắc này 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ể
Client không nên phụ thuộc vào interface mà nó không sử dụng.
Nguyên tắc này tương đối dể hiểu, thay vì gộp hết lại trong 1 interface lớn, ta có thể chia nhỏ thành nhiều interface nhỏ hơn gồm các method liên quan tới nhau, như vậy sẽ dễ quản lý hơ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
Trong đoạn code trên, class Car
có một interface được sử riêng rẽ bởi cả Driver
và Mechanic
. Ta có thể refactor lại code 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 việc tách ra là 2 interface nhỏ hơn, code ta đã đảm bảo tuân thủ nguyên tắc ISP
Dependency inversion principle
Nguyên tắc thứ 5 ứng với chữ D
trong SOLID
, nội dung nguyên tắc này như sau: 1. Các module cấp cao không nên phụ thuộc vào các modules cấp thấp. Cả 2 nên phụ thuộc vào abstraction.
2. Abstraction không nên phụ thuộc vào chi tiết, mà ngược lại.
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.
KẾT LUẬN
Trên đây là bài giới thiêu sơ qua về 5 nguyên tắc SOLID trong việc design. Hi vọng bài viết phần nào có ích trong công việc của các bạn.
No comments:
Post a Comment