Nguyên tắc SOLID trong lập trình Ruby

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 DriverMechanic. 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:

Bài viết internet: