+4

SOLID Ruby: Single Responsibility Principle

Chúng ta sử dụng kỹ thuật hướng đối tượng để tạo ra những mã code rõ ràng và đẹp mắt. Trên thực tế đây là những kết quả của mục tiêu chính: tạo ra những đoạn code với chi phí bảo trì thấp, các đoạn code không đòi hỏi nhiều thời gian, con người cho việc sửa chữa và cải tiến.

Có một nhóm các hướng dẫn và nguyên tắc giúp chúng ta đạt được mục tiêu này. Nó được gọi là SOLID và được mô tả lần đầu tiên bởi Robert Martin hơn một thập kỷ trước.

SOLID không phải là một tập hợp các quy tắc cứng nhắc, cũng không phải là nhóm các hướng dẫn duy nhất bạn nên sử dụng. Nhưng lại là một điểm khởi đầu tốt và bạn nên thay đổi cách mà bạn thiết kế và viết code phần mềm ngay bây giờ. Dưới đây là những nguyên tắc được miêu tả bởi SOLID:

  • nguyên tắc đơn nhiệm
  • nguyên tắc mở rộng - hạn chế
  • nguyên tắc thay thế Liskov
  • nguyên tắc phân tách giao tiếp
  • nguyên tắc nghịch đảo phụ thuộc

Mục tiêu của những nguyên tắc này là tạo ra những sự thay đổi mã code ít ảnh hưởng các phần còn lại. Điều đó có nghĩa là khi sửa hoặc cải tiến code nên gây ra ảnh hưởng đến các phần còn lại càng ít càng tốt. Chúng ta giảm bớt chi phí bảo trì thông qua một thiết kế được thực hiện để thích ứng với thay đổi.

Tôi mong bạn không nên có suy nghĩ sai lầm rằng: "vấn đề này là của JavaBS, chúng ta không cần nó ở đây". Thiết kế hướng đối tượng tốt áp dụng cho bất kỳ ngôn ngữ hướng đối tượng nào. Các hình thức có thể thay đổi (gợi ý: Ruby làm cho nó dễ dàng một cách vô lý để thực hiện các nguyên tắc), những ý tưởng và nguyên tắc cơ bản là giống nhau.

Một cách khác để từ chối vấn đề này là: Không có một cách duy nhất nào thực hiện tất cả các nguyên tắc này. Hãy thử một số kỹ thuật và lựa chọn những gì có ý nghĩa cho bạn dựa trên phong cách và suy nghĩ của bạn. Tôi thường chỉ ra một tùy chọn, nhưng nên nhớ còn rất rất nhiều tùy chọn khác. Nếu bạn không đồng ý xin vui lòng bày tỏ quan điểm của bạn trong các ý kiến.

Trong bài viết này tôi sẽ nói về nguyên tắc đơn nhiệm (Single Responsibility Principle - SRP). Đây là một trong những điều đơn giản nhất để hiểu và là cơ sở cho tất cả những đoạn code tốt trong lập trình hướng đối tượng.

Chú ý: tôi thường sử dụng thuật ngữ class trong bài viết này, nhưng hầu hết các trường hợp, các nguyên tắc có thể áp dụng cho bất kỳ một entity, một class, một module, một method.

SRP chỉ ra rằng, mỗi lớp chỉ nên có một responsibility và phải thực hiện đầy đủ nó, một responsibility cũng không nên chứa trong hai lớp khác nhau. Đây là một cách để đạt được gắn kết cao, một đặc điểm rất hấp dẫn trong phần mềm OO. Một lớp gắn kết một cách đầy đủ thực thi responsibility của mình, bảo vệ nó chống lại sự phân mảnh và giấu những chi tiết thực hiện.

Có một cách nghĩ về responsibility, đó là lý do để class thay đổi. Do đó chúng ta phải xác định nguyên tắc là: một class chỉ nên có một lý do để thay đổi.

Đã đến lúc thực hành, dưới đây là một class ActiveRecord đại diện cho một trò chơi:

class Game < ActiveRecord::Base
  belongs_to :category
  validates :title, :category_id, :description, :price, :platform, :year, presence: true

  def get_official_price
    open("http://thegamedatabase.com/api/game/#{name}/price?api_key=ek2o1je")
  end

  def print
    <<-EOF
      #{name}, #{platform}, #{year}

      current value is #{get_official_price}
    EOF
  end
end

Responsibility chính của class này là tạo ra logic nghiệp vụ giữa các ngữ cảnh trong game và validate các trường.

Tuy nhiên, như chúng ta có thể thấy, responsibility của class còn có đưa ra thông tin về giá và thông tin về trò chơi theo một định dạng cho trước. Sự gắn kết giữa các thành phần là khá thấp, có nhiều hơn một lý do để thay đổi:

  • validation và các mối liên hệ (Chúng ta sẽ nói thêm về ActiveRecord phá vỡ SRP sau)
  • thông tin về giá
  • xuất ra thông tin định dạng

Một cách để giải quyết vấn đề này chia lớp này ra và viết thêm vào hai lớp nữa:

class Game < ActiveRecord::Base
  belongs_to :category
  validates :title, :category_id, :description, :price, :platform, :year, presence: true
           
end

class GamePriceService
  attr_accessor :game

  # Chúng ta nên sử dụng một config file
  BASE_URL = "http://thegamedatabase.com/api/game"
  API_KEY = "ek2o1je"

  def initialize game
    self.game = game
  end

  def get_price
    data = open("#{BASE_URL}/#{game.name}/price?api_key=#{API_KEY}")
    JsonParserLib.parse data
  end
end

class GamePresenter
  attr_accessor :game

  def initialize game 
    self.game = game
  end

  def print
    price_service = GamePriceService.new game 
    <<-EOF
      #{game.name}, #{game.platform}, #{game.year}

      current value is #{price_service.get_price[:value]}
    EOF
  end
end

Bây giờ chúng ta có một lớp là chung cho mỗi responsibility được định nghĩa trước đó

Để thiết lập giá của trò chơi, bạn nên sử dụng một đối tượng game và đọc giá trị từ web service (GamePriceService). Một cách khác để làm được điều này là thông qua class Game:

class Game < ActiveRecord::Base
  belongs_to :category
  validates :title, :category_id, :description, :price, :platform, :year, presence: true
                        

  def price
    price = read_attribute :price 
    unless price.present?
      update_attribute :price, GamePriceService.new(self).get_price
      read_attribute :price 
    end
  end
end

Tôi không muốn gọi đến service bên ngoài các model ActiveRecord, nhưng đây cũng là cách để làm điều đó.

Dưới đây là danh sách các phong cách viết code có thể phá vỡ nguyên tắc SRP:

  • Class của bạn có quá nhiều comment để giải thích tại sao lại có đoạn code này.
  • Bạn sử dụng quá nhiều biến instance và thường xuyên sử dụng chúng để nhóm các phương thức giống như việc sử dụng cấu trúc dữ liệu.
  • Bạn truyền đi quá nhiều parameter trong lời gọi phương thức.
  • Thường sử dụng phương thức private và protected
  • Các phương thức trong class của bạn sử dụng nhiều câu điều kiện (if..else, case..when).

All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.