Interactor Design Pattern trong Ruby (Phần 2)
Tiếp theo của phần 1, chúng ta sẽ đi vào phân tích ví dụ về Organizer thông qua class PlaceOrder Dựa vào ví dụ ở cuối phần trước, có thể thấy mỗi interactor được thực hiện truyền qua 1 ngữ cảnh (hoặc 1 đối số) tới đối tượng tương tác tiếp theo. Ví dụ, ở organizer kể trên thì chỉ thực hiện việc đi ...
Tiếp theo của phần 1, chúng ta sẽ đi vào phân tích ví dụ về Organizer thông qua class PlaceOrder Dựa vào ví dụ ở cuối phần trước, có thể thấy mỗi interactor được thực hiện truyền qua 1 ngữ cảnh (hoặc 1 đối số) tới đối tượng tương tác tiếp theo. Ví dụ, ở organizer kể trên thì chỉ thực hiện việc đi bán hàng mà chúng tôi giả sử có rất nhiều dữ liệu về các sản phẩm và nội dung như bối cảnh của nó đối với interactor đầu tiên (CreateSale). Có thể thấy, tất cả các interactors đều làm việc như 1 "gia đình", và chúng chia sẻ bố cảnh với nhau. Có nghĩa là nếu chúng ta thiết lập 1 biến sản phẩm (param) bên trong CreateSale thì ở ProcessStock và các interactor tiếp theo cũng sẽ có quyền truy cập vào các sản phẩm thông qua đối tượng ngữ cảnh. Trừ trường hợp một trong các interactor thất bại, thì trong trường hợp đó nó sẽ tránh được các interactor tiếp theo. Sẽ có nhiều điều cần biết trong trường hợp thất bại kể trên, và bạn có thể đọc kỹ hơn trong tài liệu về Gem này. Trong tương lai (có lẽ trong phiên bản 4) chúng ta có thể vượt qua không chỉ những interactors để tổ chức thực hiện, mà còn là các đối tượng procs hoặc các method. Kiểu kiểu như ví dụ dưới đây:
class TestOrganizer include Interactor::Organizer def self.my_method puts 'here is my method' end organize [ CreateSale, ->(context) { context.result = context.arg1 + context.arg2 }, method(:my_method) ] end
Vâng, tôi nghĩ rằng thật tuyệt khi thấy các organizers chỉ nhận Proc hoặc Method Object và thực hiện nó trong khi không quan tâm đến các thành phần của đối số. Ngoài ra, tôi tin rằng việc đặt tên là rất quan trọng. Việc đặt tên cho các Interactors cũng không là ngoại lệ. Nghĩa là, toàn bộ mô tả các nhiện vụ phải được bao hàm trong tên gọi của class đó. Bạn có thể phần nào hiểu đơn giản những gì 1 interactors sẽ thực hiện chỉ bằng việc đọc tên class của chúng. Với ý tưởng này , thì khi Organizers thực hiện gọi các Interactors, nó sẽ trở nên rất rõ ràng, kiểu như là CreateSale, ProcessStock, DispatchShipping, SendConfirmationEmail. Bây giờ, nếu một Proc, ngay cả khi nó có 1 từ nối đứng ở giữa "câu"? Nó chắc chắn sẽ không giống như trước. Nó không phải là thực sự cần thiết, mặc dù trong các trường hợp thích hợp hơn, tôi đoán nó vẫn có thể đọc hiểu được.
Những điểm nổi bật
Mã Nguồn của Gem này rất nhỏ. Chỉ cần có 1 vài điểm chú ý, thư mục lib có 5 files Ruby. Method dài nhất là 8 dòng. Chỉ có 1 vài điều khiện và không có "elses". Cũng không có meta-progamming, làm cho nó rất dễ dàng để hiểu toàn bộ cơ sở của source code chỉ bằng cách đọc nó, ngay cả khi bạn không thực sự hiểu sâu về Ruby. Và tính đơn giản ở đây không phải là sự trùng hợp, đó là do thiết kế. VÍ dụ, đã có những đề xuất để thực hiện xác việc validation bên trong các interactors trong quá khứ. Ý tưởng ở đây rất đơn giản: interactor cần phải xác thực ngữ cảnh của nó trước khi thực hiện nhiệm vụ chính. Và một số gem khác có được từ những cuộc thảo luận sâu về chủ đề này.
Vì 1 interactor luôn cố gắng cho sự đơn giản và "sang trọng" ngày từ đầu tieen, nên phải có 1 cách "tao nhã" để cung cấp khả năng xác nhận ngữ cảnh mà không cần chèn quá nhiều dòng code vào source code. Vâng, đôi đã suy nghĩ như vậy, vì đối tượng ngữ cảnh phải là khép kín. Và Interactor đã cũng cấp các phương pháp cho việc validate. Cụ thể hơn chúng ta đi vào 1 ví dụ:
# controller - option 1 def create sale = Sale.find params[:id] # this option makes context and validator explicitly separated parameters SaveSale.call(context: {sale: sale}, validator: ValidateSaveSale) end # controller - option 2 def create sale = Sale.find params[:id] # this option allows to simply pass the validator along within the context SaveSale.call(sale: sale, validator: ValidateSaveSale) end #validator interactor class SaleValidator include Interactor def call context.fail! unless context.sale.present? && context.sale.errors.empty? end end #business interactor class SaveSale include Interactor before do # some code would be necessary in the gem to pull the validator out of the context as a separate component context.fail! if validator.call(context).failed? end def call #… end end
Lưu ý rằng, ý tưởng này đưa ra khái niệm về 1 Interacotr Validation, mà không ngạc nhiên khi thông qua các params và validates các interactor cụ thể. Trong đoạn mã ở trên, tôi trình bày 2 cách để truyền 1 validator cho phương thức được gọi đến. Việc đầu tiên sẽ gây ra một sự thay đổi trong API, dó là luôn luôn khó hơn để giải quyết. Nhưng nó giữ lại bối cảnh và validator như 2 thành phần riêng bietj cho interactor chạy. Thực tế là có một tham số riêng biệt cho trình xác nhận chỉ làm rõ ràng hơn rằng trình xác nhận không phải là một phần của ngữ cảnh, nhưng một đối tượng khác đã được tiêm và chỉ đơn giản biết làm thế nào để xác nhận anh trai, đối tượng ngữ cảnh. Lựa chọn thứ hai đơn giản hơn. Trình xác nhận là một phần của ngữ cảnh và viên ngọc cần phải làm việc một chút để kéo trình xác nhận ra khỏi đối tượng ngữ cảnh. Điều này cũng đòi hỏi một tài liệu tốt hơn, một khi trình xác nhận là tiềm ẩn.
Điểm yếu
Biến toàn cục
Một điều mà tôi đã luôn luôn tìm thấy loại vụng về là Context đối tượng được truyền như là tham số trong phương thức gọi đến. Về cơ bản nó là một đối tượng struct rất promiscuous, sẽ chấp nhận bất cứ điều gì bất cứ lúc nào. Nó là một biến toàn cầu. Và bạn biết những gì đã xảy ra với cậu bé chơi với các biến toàn cục, phải không? Yeah, anh ta qua đời =)). Vì vậy, hãy ở lại, xin vui lòng. Tất nhiên tôi phóng đại, thường là không có vấn đề lớn với các tác tử mang biến toàn cục xung quanh (trừ khi bạn là một người purist). Thường thì chúng ngắn ngủi và thực tế là chúng chỉ có một trách nhiệm duy nhất giúp ích rất nhiều. Nhưng tôi có thể thấy nó trở nên lộn xộn. Hãy tưởng tượng rằng chúng tôi có một người tổ chức gồm 5 người tương tác, mỗi người có một thiết lập hookback riêng. Khi chúng ta vượt qua các giá trị xung quanh trong một đối tượng ngữ cảnh, với mỗi một tương tác có thể thay đổi, thêm hoặc xóa các đối tượng từ đối tượng ngữ cảnh chúng ta có thể gặp vấn đề. Bây giờ, nếu có thể tạo ra lớp ngữ cảnh tùy chỉnh riêng của chúng ta, chúng ta có thể quyết định những gì có thể được truy cập hoặc đọc trong ngữ cảnh. Mặt khác, nếu bạn không nhớ bối cảnh toàn cầu mà mọi người đang truy cập, hoặc bạn không cần nó đối với nhiệm vụ bạn đang làm là sống ngắn và đơn giản, chỉ cần sử dụng một mặc định và bạn tốt đi.
Chưa có một cách rõ ràng hơn để validation tham số
Như đã giải thích trước, không có cách nào rõ ràng để xác thực đối tượng ngữ cảnh bên trong một trình tương tác. Ý tưởng được giải thích ở trên là sử dụng cái mà tôi gọi là Validator Interactor. Đó là một ý tưởng đơn giản, không có phụ thuộc và không "vết" triết lý đằng sau các chất tương tác bất cứ điều gì. Nhưng, như trong hầu hết các trường hợp đơn giản, nó có thể là quá đơn giản. Một giải pháp khác là đi ngược lại: "gây ô nhiễm" mã một chút để cung cấp cơ chế kiểm tra ngữ cảnh linh hoạt bên trong trình tương tác. Một cái gì đó theo các ý tưởng về hợp đồng, như hợp đồng tương tác và đoàn kịch:
class MyInteractor include Troupe expects :property1, :property2 on_violation do |violation| if violation.property == :property1 puts "Property1 violated the contract!" else context.fail!(error: violation.message) end end end
Tổng kết
Như chúng ta đã thấy, các interactors giải quyết vấn đề lập trình thực sự, giúp tránh Fat Controllers và Fat Models quá mức. Pattern này lấy cảm hứng từ Command Pattern và cung cấp một cách để tạo ra một sự trừu tượng hơn các model cơ bản của chúng ta và gói gọn trông các business logic. Bằng cách đó, rất nhiều mã thao tác các đối tượng mô hình của chúng tôi, thường được viết bên trong các model hoặc thậm chí trong các controler, có thể được di chuyển sang các interactor, làm cho mã của chúng ta theo kiểu module và gọn hơn. Trên thực tế, tại Guava, chúng tôi không thích Fat Models và Fat Controllers, nhưng chúng tôi ủng hộ việc tạo ra nhiều tác nhân tương tác bất cứ khi nào hợp lý để làm như vậy. Và không, bạn có thể không cần Gem để bắt đầu tạo ra những interactors của riêng mình. Hãy nhớ rằng: đó là một Service Object cơ bản với một trách nhiệm và một phương thức public. Chỉ cần giữ nó đơn giản. Mặt khác, nếu bạn cảm thấy như được hướng dẫn bằng giọng nói, hãy thử các Gem Interactor khác. Nó khá gọn gàng. Rất nhỏ cơ sở mã, không có phép thuật, chỉ cần đơn giản Ruby. Ngoài ra, rất đơn giản để cắm vào các lớp dịch vụ của bạn và bắt đầu sử dụng. Cố gắng sử dụng tốt các tính năng sắc nét của nó như móc. Nhưng hãy nhớ rằng interactor cũng có những khuyết điểm của nó. Tôi đã đề cập đến một vài trong số đó ở trên, cùng với một số kỹ thuật để đi xung quanh họ một cách thanh lịch. Tuy nhiên, một số vấn đề vẫn còn phải đợi một giải pháp chính thức. Oh, tốt ... pobody là nerfect.