Clean Code Ruby - Objects and Data Structures, Classes, SOLID

Tiếp nối chuối series clean code ruby, kỳ này là phần nói về làm thế nào để clean Objects and Data Structures, Classes, SOLID. Mời các bạn đón đọc nhé

Objects and Data Structures

Sử dụng getters và setters

Sử dụng getters và setters để truy cập dữ liệu trên các đối tượng có thể tốt hơn là chỉ đơn giản là tìm kiếm một thuộc tính trên một đối tượng. Dưới đây là những lý do tại sao:

  • Khi bạn muốn làm nhiều hơn ngoài việc chỉ có một thuộc tính đối tượng, bạn không phải tìm kiếm và thay đổi mọi truy cập trong cơ sở mã của bạn.
  • Làm cho việc xác nhận đơn giản hơn.
  • Đóng gói các đại diện nội bộ.
  • Dễ dàng thêm ghi log và xử lý lỗi khi getting và setting.
  • Bạn có thể lười tải các thuộc tính của đối tượng, thì có thể lấy nó từ máy chủ.
def make_bank_account
  # ...

  {
    balance: 0
    # ...
  }
end

account = make_bank_account
account[:balance] = 100
account[:balance] # => 100

Good:

class BankAccount
  def initialize
    # this one is private
    @balance = 0
  end

  # a "getter" via a public instance method
  def balance
    # do some logging
    @balance
  end

  # a "setter" via a public instance method
  def balance=(amount)
    # do some logging
    # do some validation
    @balance = amount
  end
end

account = BankAccount.new
account.balance = 100
account.balance # => 100

Ngoài ra, nếu getters và setters của bạn xử lý hoàn toàn bình thường, bạn nên sử dụng attr_accessor để định nghĩa chúng. Điều này đặc biệt thuận tiện cho việc triển khai các đối tượng, giống như việc hiển thị dữ liệu cho các phần khác của hệ thống.

Good:

class Toy
  attr_accessor :price
end

toy = Toy.new
toy.price = 50
toy.price # => 50

Tuy nhiên, bạn phải lưu ý rằng trong một số tình huống, sử dụng attr_accessor khiến code của bạn bốc mùi, hãy đọc thêm tại đây.

Classes

Tránh Fluent interface

Fluent interface là một API hướng đối tượng nhằm cải thiện khả năng đọc mã nguồn bằng cách sử dụng chuỗi phương thức..

Mặc dù có thể trong một số trường hợp, các đối tượng xây dựng thường xuyên, trong đó mẫu này làm giảm tính dài dòng của mã (ví dụ: các truy vấn ActiveRecord), thường thì nó có chi phí:

Để biết thêm thông tin, bạn có thể đọc toàn bộ bài đăng trên blog về chủ đề này được viết bởi Marco Pivetta.

Bad:

class Car
  def initialize(make, model, color)
    @make = make
    @model = model
    @color = color
    # NOTE: Returning self for chaining
    self
  end

  def set_make(make)
    @make = make
    # NOTE: Returning self for chaining
    self
  end

  def set_model(model)
    @model = model
    # NOTE: Returning self for chaining
    self
  end

  def set_color(color)
    @color = color
    # NOTE: Returning self for chaining
    self
  end

  def save
    # save object...
    # NOTE: Returning self for chaining
    self
  end
end

car = Car.new('Ford','F-150','red')
  .set_color('pink')
  .save

Good:

class Car
  attr_accessor :make, :model, :color

  def initialize(make, model, color)
    @make = make
    @model = model
    @color = color
  end

  def save
    # Save object...
  end
end

car = Car.new('Ford', 'F-150', 'red')
car.color = 'pink'
car.save

Thích các thành phần hơn thừa kế

Như đã nêu trong cuốn sách nổi tiếng Mẫu thiết kế của Gang of Four, bạn nên thích sáng tác hơn là kế thừa nơi bạn có thể. Có rất nhiều lý do tốt để sử dụng thừa kế và rất nhiều lý do tốt để sử dụng thành phần. Điểm chính của câu châm ngôn này là nếu tâm trí của bạn theo bản năng đi theo sự kế thừa, hãy thử nghĩ xem liệu bố cục có thể mô hình hóa vấn đề của bạn tốt hơn không. Trong một số trường hợp, nó có thể.

Sau đó, bạn có thể tự hỏi, "khi nào tôi nên sử dụng thừa kế?" Nó phụ thuộc vào vấn đề của bạn, nhưng đây là một danh sách hợp lý khi kế thừa có ý nghĩa hơn thành phần:

  • Các class của bạn đại diện cho mối quan hệ "is-a" chứ không phải mối quan hệ "has-a" (Human->Animal vs. User->UserDetails).
  • Bạn có thể sử dụng lại mã từ các lớp cơ sở (Con người có thể di chuyển như tất cả các loài động vật).
  • Bạn muốn thực hiện các thay đổi cho các lớp con bằng cách thay đổi một lớp cơ sở. (Thay đổi chi phí calo của tất cả các động vật khi chúng di chuyển).

Bad:

class Employee
  def initialize(name, email)
    @name = name
    @email = email
  end

  # ...
end

# Bad because Employees "have" tax data. EmployeeTaxData is not a type of Employee
class EmployeeTaxData < Employee
  def initialize(ssn, salary)
    super()
    @ssn = ssn
    @salary = salary
  end

  # ...
end

Good:

class EmployeeTaxData
  def initialize(ssn, salary)
    @ssn = ssn
    @salary = salary
  end

  # ...
end

class Employee
  def initialize(name, email)
    @name = name
    @email = email
  end

  def set_tax_data(ssn, salary)
    @tax_data = EmployeeTaxData.new(ssn, salary)
  end
  # ...
end

SOLID

Nguyên tắc đơn nhiệm (SRP)

Như đã nêu trong Clean Code, "Không bao giờ nên có nhiều hơn một lý do để một lớp thay đổi". Thật hấp dẫn khi đóng gói một lớp học với rất nhiều chức năng, như khi bạn chỉ có thể mang theo một chiếc vali trên chuyến bay của mình. Vấn đề với điều này là lớp của bạn sẽ không gắn kết về mặt khái niệm và điều đó sẽ cho nó nhiều lý do để thay đổi. Giảm thiểu số lần bạn cần thay đổi một lớp là rất quan trọng vì nếu có quá nhiều chức năng trong một lớp, khi bạn sửa đổi một phần của nó, có thể làm ảnh hưởng đến các mô-đun phụ thuộc khác trong cơ sở mã của bạn.

Bad:

class UserSettings
  def initialize(user)
    @user = user
  end

  def change_settings(settings)
    return unless valid_credentials?
    # ...
  end

  def valid_credentials?
    # ...
  end
end

Good:

class UserAuth
  def initialize(user)
    @user = user
  end

  def valid_credentials?
    # ...
  end
end

class UserSettings
  def initialize(user)
    @user = user
    @auth = UserAuth.new(user)
  end

  def change_settings(settings)
    return unless @auth.valid_credentials?
    # ...
  end
end

Nguyên tắc mở / đóng (OCP)

Như Bertrand Meyer đã nêu ra, "các thực thể phần mềm (lớp, mô-đun, hàm, v.v.) nên được mở để mở rộng, nhưng đóng để sửa đổi." Điều đó có nghĩa là gì? Nguyên tắc này về cơ bản nói rằng bạn nên cho phép người dùng thêm các chức năng mới mà không thay đổi mã hiện có.

Bad:

class Adapter
  attr_reader :name
end

class AjaxAdapter < Adapter
  def initialize
    super()
    @name = 'ajaxAdapter'
  end
end

class NodeAdapter < Adapter
  def initialize
    super()
    @name = 'nodeAdapter'
  end
end

class HttpRequester
  def initialize(adapter)
    @adapter = adapter
  end

  def fetch(url)
    case @adapter.name
    when 'ajaxAdapter'
      make_ajax_call(url)
    when 'nodeAdapter'
      make_http_call(url)
    end
  end

  def make_ajax_call(url)
    # ...
  end

  def make_http_call(url)
    # ...
  end
end

Good:

class Adapter
  attr_reader :name
end

class AjaxAdapter < Adapter
  def initialize
    super()
    @name = 'ajaxAdapter'
  end

  def request(url)
    # ...
  end
end

class NodeAdapter < Adapter
  def initialize
    super()
    @name = 'nodeAdapter'
  end

  def request(url)
    # ...
  end
end

class HttpRequester
  def initialize(adapter)
    @adapter = adapter
  end

  def fetch(url)
    @adapter.request(url)
  end
end

Nguyên tắc thay thế Liskov (LSP)

Đây là một thuật ngữ đáng sợ cho một khái niệm rất đơn giản. Nó được định nghĩa chính thức là "Nếu S là một kiểu con của T, thì các đối tượng thuộc loại T có thể được thay thế bằng các đối tượng loại S (nghĩa là các đối tượng loại S có thể thay thế các đối tượng thuộc loại T) mà không làm thay đổi bất kỳ thuộc tính mong muốn nào của chương trình đó (tính đúng đắn, nhiệm vụ được thực hiện, v.v.). " Đó là một định nghĩa thậm chí còn đáng sợ hơn.

Giải thích tốt nhất cho điều này là nếu bạn có một lớp cha và một lớp con, thì lớp cơ sở luôn có thể được thay thế bởi lớp con mà không nhận được kết quả không chính xác. Điều này có thể vẫn còn gây nhầm lẫn, vì vậy hãy xem ví dụ về Hình chữ nhật vuông cổ điển. Về mặt toán học, hình vuông là một hình chữ nhật, nhưng nếu bạn mô hình hóa nó bằng cách sử dụng mối quan hệ "is-a" thông qua thừa kế, bạn sẽ nhanh chóng gặp rắc rối.

Bad:

class Rectangle
  def initialize
    @width = 0
    @height = 0
  end

  def color=(color)
    # ...
  end

  def render(area)
    # ...
  end

  def width=(width)
    @width = width
  end

  def height=(height)
    @height = height
  end

  def area
    @width * @height
  end
end

class Square < Rectangle
  def width=(width)
    @width = width
    @height = width
  end

  def height=(height)
    @width = height
    @height = height
  end
end

def render_large_rectangles(rectangles)
  rectangles.each do |rectangle|
    rectangle.width = 4
    rectangle.height = 5
    area = rectangle.area # BAD: Returns 25 for Square. Should be 20.
    rectangle.render(area)
  end
end

rectangles = [Rectangle.new, Rectangle.new, Square.new]
render_large_rectangles(rectangles)

Good:

class Shape
  def color=(color)
    # ...
  end

  def render(area)
    # ...
  end
end

class Rectangle < Shape
  def initialize(width, height)
    super()
    @width = width
    @height = height
  end

  def area
    @width * @height
  end
end

class Square < Shape
  def initialize(length)
    super()
    @length = length
  end

  def area
    @length * @length
  end
end

def render_large_shapes(shapes)
  shapes.each do |shape|
    area = shape.area
    shape.render(area)
  end
end

shapes = [Rectangle.new(4, 5), Rectangle.new(4, 5), Square.new(5)]
render_large_shapes(shapes)

Nguyên tắc phân chia giao diện (ISP)

Ruby không có giao diện nên nguyên tắc này không áp dụng nghiêm ngặt như các ngôn ngữ khác. Tuy nhiên, nó quan trọng và phù hợp ngay cả với hệ thống thiếu loại của Ruby.

ISP tuyên bố rằng "Khách hàng không nên bị buộc phải phụ thuộc vào các giao diện mà họ không sử dụng."

Khi một máy khách phụ thuộc vào một lớp có chứa các giao diện mà máy khách không sử dụng, nhưng các máy khách khác sử dụng, thì máy khách đó sẽ bị ảnh hưởng bởi những thay đổi mà các máy khách khác thay đổi đối với lớp đó.

Bạn có thể tham khảo ở ví dụ sau đây

Bad:

class Car
  # used by Driver
  def open
    # ...
  end

  # used by Driver
  def start_engine
    # ...
  end

  # used by Mechanic
  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

Good:

# used by Driver only
class Car
  def open
    # ...
  end

  def start_engine
    # ...
  end
end

# used by Mechanic only
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

Nguyên tắc đảo ngược phụ thuộc (DIP)

Nguyên tắc này nêu hai điều thiết yếu:

  • Các mô-đun cấp cao không nên phụ thuộc vào các mô-đun cấp thấp. Cả hai nên phụ thuộc vào trừu tượng.
  • Trừu tượng không nên phụ thuộc vào chi tiết. Chi tiết nên phụ thuộc vào trừu tượng.

Nói một cách đơn giản, DIP giữ cho các mô-đun cấp cao không biết chi tiết về các mô-đun cấp thấp và thiết lập chúng. Một lợi ích rất lớn của việc này là nó làm giảm sự khớp nối giữa các mô-đun. Khớp nối là một mô hình phát triển rất tệ vì nó làm cho mã của bạn khó tái cấu trúc.

Như đã nói ở trên, Ruby không có giao diện nên các khái niệm trừu tượng phụ thuộc vào các hợp đồng ngầm. Điều đó có nghĩa là, các phương thức và thuộc tính mà một đối tượng / lớp tiếp xúc với một đối tượng / lớp khác. Trong ví dụ dưới đây, hợp đồng ngầm định là bất kỳ mô-đun Yêu cầu nào cho InventoryTracker sẽ có phương thức request_items.

Bad:

class InventoryRequester
  def initialize
    @req_methods = ['HTTP']
  end

  def request_item(item)
    # ...
  end
end

class InventoryTracker
  def initialize(items)
    @items = items

    # BAD: We have created a dependency on a specific request implementation.
    @requester = InventoryRequester.new
  end

  def request_items
    @items.each do |item|
      @requester.request_item(item)
    end
  end
end

inventory_tracker = InventoryTracker.new(['apples', 'bananas'])
inventory_tracker.request_items

Good:

class InventoryTracker
  def initialize(items, requester)
    @items = items
    @requester = requester
  end

  def request_items
    @items.each do |item|
      @requester.request_item(item)
    end
  end
end

class InventoryRequesterV1
  def initialize
    @req_methods = ['HTTP']
  end

  def request_item(item)
    # ...
  end
end

class InventoryRequesterV2
  def initialize
    @req_methods = ['WS']
  end

  def request_item(item)
    # ...
  end
end

# By constructing our dependencies externally and injecting them, we can easily
# substitute our request module for a fancy new one that uses WebSockets.
inventory_tracker = InventoryTracker.new(['apples', 'bananas'], InventoryRequesterV2.new)
inventory_tracker.request_items

Link Tham khảo

https://github.com/uohzxela/clean-code-ruby#objects-and-data-structures https://github.com/uohzxela/clean-code-ruby#classes https://github.com/uohzxela/clean-code-ruby#solid


All Rights Reserved