12/08/2018, 14:50

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:

  1. Single Responsibility Principle
  2. Open/Closed Principle
  3. Liskov Substitution Principle
  4. Interface Segregation Principle
  5. 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:

  1. 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.
  2. 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

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

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

0