Sunday, May 8, 2022

 

Á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.

Tài liệu tham khảo

  1. (https://subvisual.co/blog/posts/19-solid-principles-in-ruby/)[https://subvisual.co/blog/posts/19-solid-principles-in-ruby/]

No comments:

Post a Comment

So sánh các GitFlow model và áp dụng với CICD

https://medium.com/oho-software/so-s%C3%A1nh-c%C3%A1c-gitflow-model-v%C3%A0-%C3%A1p-d%E1%BB%A5ng-v%E1%BB%9Bi-cicd-b6581cfc893a