Computer >> Máy Tính >  >> Lập trình >> Ruby

Tách Ruby:Ủy quyền và Phụ thuộc Injection

Trong Lập trình hướng đối tượng , một đối tượng thường sẽ phụ thuộc vào một đối tượng khác để hoạt động.

Ví dụ:nếu tôi tạo một lớp đơn giản để chạy báo cáo tài chính:

class FinanceReport
  def net_income
    FinanceApi.gross_income - FinanceApi.total_costs
  end
end

Có thể nói rằng FinanceReport phụ thuộc vào FinanceApi , nó sử dụng để lấy thông tin từ bộ xử lý thanh toán bên ngoài.

Nhưng điều gì sẽ xảy ra nếu chúng ta muốn sử dụng một API khác tại một thời điểm nào đó? Hoặc, nhiều khả năng hơn, điều gì sẽ xảy ra nếu chúng ta muốn kiểm tra lớp này mà không cần đến các nguồn lực bên ngoài? Câu trả lời phổ biến nhất là sử dụng Dependency Injection.

Với Dependency Injection, chúng tôi không tham chiếu rõ ràng đến FinanceApi bên trong FinanceReport . Thay vào đó, chúng tôi chuyển nó vào như một đối số. Chúng tôi tiêm nó.

Sử dụng Dependency Injection, lớp của chúng ta sẽ trở thành:

class FinanceReport
  def net_income(financials)
    financials.gross_income - financials.total_costs
  end
end

Bây giờ lớp chúng tôi không biết rằng FinanceApi đối tượng thậm chí tồn tại! Chúng tôi có thể vượt qua bất kỳ đối tượng nào với nó miễn là nó triển khai gross_incometotal_costs .

Điều này có một số lợi ích:

  • Mã của chúng tôi hiện ít được "ghép nối" với FinanceApi .
  • Chúng tôi buộc phải sử dụng FinanceApi thông qua giao diện công khai.
  • Giờ đây, chúng tôi có thể chuyển đối tượng giả hoặc đối tượng sơ khai trong các thử nghiệm của mình để chúng tôi không phải sử dụng API thực.

Hầu hết các nhà phát triển coi Dependency Injection nói chung là một điều tốt (tôi cũng vậy!). Tuy nhiên, như với tất cả các kỹ thuật, có sự đánh đổi.

Mã của chúng tôi bây giờ hơi mờ đục hơn một chút. Khi chúng tôi sử dụng FinanceApi một cách rõ ràng , rõ ràng giá trị của chúng tôi đến từ đâu. Nó không hoàn toàn rõ ràng trong mã kết hợp Dependency Injection.

Nếu khác, các cuộc gọi sẽ được chuyển đến self , sau đó chúng tôi đã làm cho mã dài hơn. Thay vì sử dụng mô hình Hướng đối tượng "gửi thông báo đến một đối tượng và để nó hoạt động", chúng tôi thấy mình đang chuyển sang mô hình "đầu vào -> đầu ra" chức năng hơn.

Đây là trường hợp cuối cùng (chuyển hướng cuộc gọi mà lẽ ra phải đến self ) mà tôi muốn xem hôm nay. Tôi muốn trình bày một giải pháp thay thế có thể có cho Dependency Injection cho các trường hợp sau: thay đổi động lớp cơ sở (kinda).

Vấn đề cần giải quyết

Hãy sao lưu lại một chút và bắt đầu với vấn đề khiến tôi đi đến con đường này để bắt đầu với:báo cáo PDF.

Khách hàng của tôi đã yêu cầu khả năng tạo các báo cáo PDF có thể in khác nhau - một báo cáo liệt kê tất cả các chi phí cho một tài khoản, một doanh thu niêm yết khác, một báo cáo khác về lợi nhuận dự báo cho những năm trong tương lai, v.v.

Chúng tôi đang sử dụng prawn đáng kính gem để tạo các tệp PDF này, với mỗi báo cáo là đối tượng Ruby của riêng nó được phân lớp từ Prawn::Document .

Một cái gì đó như thế này:

class CostReport < Prawn::Document
  def initialize(...)
    ...
  end

  def render
    text "Cost Report"
    move_down 20
    ...
  end

Càng xa càng tốt. Nhưng đây là điểm cần thiết:khách hàng muốn có báo cáo "Tổng quan" bao gồm các phần từ tất cả các báo cáo khác này .

Giải pháp 1:Truyền phụ thuộc

Như đã đề cập trước đây, một giải pháp phổ biến cho loại vấn đề này là cấu trúc lại mã để sử dụng Dependency Injection. Nghĩa là, thay vì có tất cả các phương thức gọi báo cáo này trên self , thay vào đó, chúng tôi sẽ chuyển vào tài liệu PDF của mình như một đối số.

Điều này sẽ cung cấp cho chúng tôi một cái gì đó giống như:

class CostReport < Prawn::Document
...
  def title(pdf = self)
    pdf.text "Cost Report"
    pdf.move_down 20
    ...
  end
end

Điều này hoạt động, nhưng có một số chi phí ở đây. Đối với một điều, mọi phương pháp vẽ đơn lẻ bây giờ phải sử dụng pdf đối số và mọi lệnh gọi đến prawn bây giờ phải chuyển qua pdf này đối số.

Việc tiêm phụ thuộc có một số lợi ích:nó thúc đẩy chúng ta tiến tới các thành phần được tách rời trong hệ thống của mình và cho phép chúng ta chuyển các đoạn mô phỏng hoặc sơ khai để làm cho việc kiểm tra đơn vị dễ dàng hơn.

Tuy nhiên, chúng ta không gặt hái được thành quả từ những lợi ích này trong kịch bản của chúng ta. Chúng tôi đã mạnh mẽ cùng với prawn API, vì vậy việc thay đổi sang một thư viện PDF khác gần như chắc chắn sẽ yêu cầu viết lại toàn bộ mã.

Việc kiểm tra cũng không phải là một mối quan tâm lớn ở đây, vì trong trường hợp của chúng tôi, việc kiểm tra các báo cáo PDF được tạo bằng các bài kiểm tra tự động là quá cồng kềnh nên không đáng giá.

Vì vậy, Dependency Injection mang lại cho chúng tôi hành vi mà chúng tôi muốn nhưng cũng giới thiệu chi phí bổ sung với lợi ích tối thiểu cho chúng tôi. Hãy xem xét một tùy chọn khác.

Giải pháp 2:Ủy quyền

Thư viện tiêu chuẩn của Ruby cung cấp cho chúng ta SimpleDelegator như một cách dễ dàng để triển khai mẫu trang trí. Bạn chuyển đối tượng của mình cho phương thức khởi tạo và sau đó bất kỳ lệnh gọi phương thức nào tới trình ủy quyền đều được chuyển tiếp đến đối tượng của bạn.

Sử dụng SimpleDelegator , chúng ta có thể tạo một lớp báo cáo cơ sở bao quanh prawn .

class PrawnWrapper < SimpleDelegator
  def initialize(document: nil)
    document ||= Prawn::Document.new(...)
    super(document)
  end
end

Sau đó, chúng tôi có thể cập nhật các báo cáo của mình để kế thừa từ lớp này và chúng sẽ vẫn hoạt động giống như trước đây, bằng cách sử dụng tài liệu mặc định được tạo trong bộ khởi tạo của chúng tôi. Điều kỳ diệu xảy ra khi chúng tôi sử dụng điều này trong tổng quan của chúng tôi báo cáo:

class OverviewReport < PrawnWrapper
  ...
  def render
    sales = SaleReport.new(..., document: self)
    sales.sales_table
    costs = CostReport.new(..., document: self)
    costs.costs_pie_chart
    ...
  end
end

Đây SaleReport#sales_tableCostReport#costs_pie_chart vẫn không thay đổi, nhưng các cuộc gọi của họ đến prawn (ví dụ:text(...) , move_down 20 , v.v.) hiện đang được chuyển tiếp đến OverviewReport thông qua SimpleDelegator chúng tôi đã tạo.

Về mặt hành vi, về cơ bản chúng tôi đã làm cho nó như thể SalesReport hiện là một lớp con của OverviewReport . Trong trường hợp của chúng tôi, điều này có nghĩa là tất cả các lệnh gọi đến prawn API của bây giờ chuyển đến SalesReport -> OverviewReport -> Prawn::Document .

Cách SimpleDelegator hoạt động

Cách SimpleDelegator hoạt động ngầm về cơ bản là sử dụng method_missing của Ruby chức năng chuyển tiếp các cuộc gọi phương thức tới một đối tượng khác.

Vì vậy, SimpleDelegator (hoặc một lớp con của nó) nhận một cuộc gọi phương thức. Nếu nó thực hiện phương pháp đó, tuyệt vời; nó sẽ thực thi nó giống như bất kỳ đối tượng nào khác. Tuy nhiên , nếu không có phương thức đó được xác định, thì nó sẽ nhấn method_missing . method_missing sau đó sẽ cố gắng call phương thức đó trên đối tượng được cung cấp cho phương thức khởi tạo của nó.

Một ví dụ đơn giản:

require 'simple_delegator'
class Thing
  def one
    'one'
  end
  def two
    'two'
  end
end

class ThingDecorator < SimpleDelegator
  def two
    'three!'
  end
end

ThingDecorator.new(Thing.new).one #=> "one"
ThingDecorator.new(Thing.new).two #=> "three!"

Bằng cách phân lớp SimpleDelegator với ThingDecorator của riêng chúng tôi ở đây, chúng ta có thể ghi đè một số phương thức và để những phương thức khác chuyển sang Thing mặc định đối tượng.

Ví dụ nhỏ ở trên không thực sự làm được SimpleDelegator công lý, mặc dù. Bạn có thể nhìn vào mã này và rất hay nói với tôi, “Không phân lớp Thing cho tôi kết quả tương tự? ”

Vâng, vâng, nó có. Nhưng đây là điểm khác biệt chính:SimpleDelegator lấy đối tượng mà nó sẽ ủy quyền làm đối số trong phương thức khởi tạo của nó. Điều này có nghĩa là chúng tôi có thể chuyển các đối tượng khác nhau trong thời gian chạy .

Đây là những gì cho phép sử dụng để chuyển hướng các cuộc gọi đến prawn đối tượng trong Giải pháp 2 ở trên. Nếu chúng ta gọi một báo cáo duy nhất là prawn các cuộc gọi đến một tài liệu mới được tạo trong phương thức khởi tạo. Tuy nhiên, báo cáo tổng quan có thể thay đổi điều này để gọi đến prawn được chuyển tiếp đến tài liệu.

Kết luận

Dependency Injection có lẽ là giải pháp tốt nhất cho hầu hết sự cố tách rời hầu hết của thời gian.

Tuy nhiên, đối với tất cả các kỹ thuật, đều có sự đánh đổi. Trong trường hợp của tôi, tôi không nghĩ rằng chi phí do DI giới thiệu là xứng đáng với những lợi ích mà nó mang lại, vì vậy tôi đã tìm kiếm một giải pháp khác.

Như với tất cả mọi thứ trong Ruby, luôn có một cách khác . Tôi không thường xuyên tìm đến giải pháp này, nhưng chắc chắn đây là một bổ sung tuyệt vời cho dây công cụ Ruby của bạn cho những trường hợp này.