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_income
và total_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_table
và CostReport#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 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.