Service Objects trong Ruby on Rails
Hãy bắt đầu bằng cách kêu gọi thực tế rằng chúng tôi đang đặt một loạt các trách nhiệm khác nhau vào một Class Service. Thêm vào đó, nó không thực sự theo các lỗi hoặc thành công thông qua lớp cha vào controller yêu cầu Service. Để bắt đầu khắc phục, chúng ta sẽ phân chia từng trách nhiệm vào các ...
Hãy bắt đầu bằng cách kêu gọi thực tế rằng chúng tôi đang đặt một loạt các trách nhiệm khác nhau vào một Class Service. Thêm vào đó, nó không thực sự theo các lỗi hoặc thành công thông qua lớp cha vào controller yêu cầu Service. Để bắt đầu khắc phục, chúng ta sẽ phân chia từng trách nhiệm vào các lớp riêng của chúng.
Trước khi hiển thị code cho các lớp này, tôi cũng muốn chạm vào một thay đổi khác trong service object mới, sử dụng cấu trúc để chèn các lớp phụ thuộc:
class NewRegistration def self.build new OrganizationSetup.build, AdminUserSetup.build, SendWelcomeEmail.build, NotifySlack.build end def initialize organization_setup, admin_user_setup, send_welcome_email, notify_slack self.organization_setup = organization_setup self.admin_user_setup = admin_user_setup self.send_welcome_email = send_welcome_email self.notify_slack = notify_slack end ....
Với ý tưởng có thể gọi một method xây dựng trên một class Service để tạo nhanh chóng đối tượng và bất kỳ người phụ thuộc. Nếu đó là một lớp con rỗng, bạn chỉ cần gọi mới. Bằng cách này các lớp được inject có thể được truyền vào, thay vì hardcode và có thể trả riêng lẻ khi thiết lập các đối tượng của bạn để được kiểm tra.
Nếu không có thêm ado, đây là các lớp con mới:
... thiết lập tổ chức
# app/services/new_registration/organization_setup.rb class NewRegistration class OrganizationSetup def self.build new end def call(organization) organization.save! end end end
... thiết lập người dùng ban đầu từ tổ chức mới được tạo ra
# app/services/new_registration/admin_user_setup.rb class NewRegistration class AdminUserSetup def self.build new end def call(user, organization) user.organization_id = organization.id user.save user.add_role :admin, organization end end end
... gửi email chào mừng
# app/services/new_registration/send_welcome_email.rb class NewRegistration class SendWelcomeEmail def self.build new end def call(user) WelcomeEmailMailer.welcome_email(user).deliver_later end end end
... và cuối cùng, ping slack
# app/services/new_registration/notify_slack.rb class NewRegistration class NotifySlack def self.build new end def call(user, organization) notifier = Slack::Notifier.new "https://hooks.slack.com/services/89hiusdfhiwufhsdf89" notifier.ping "A New User has appeared! #{organization.name} - #{user.name} || ENV: #{Rails.env}" end end end
Trong phiên bản mới của services tôi đã chia nhỏ các thành phần con một chút khác nhau để thể hiện tốt hơn từng services con riêng lẻ và có khả năng xử lý ngoại lệ. Bây giờ, chúng tôi có services con của chúng tôi, chúng tôi có thể gọi cho chúng trong services cha của chúng tôi
# app/services/new_registration.rb def call(params) user = params[:user] organization = params[:organization] begin organization_setup.call(organization) admin_user_setup.call(user, organization) send_welcome_email.call(user) notify_slack.call(user, organization) rescue => exception OpenStruct(success?: false, user: user, organization: organization, error: exception) else OpenStruct(success?: true, user: user, organization: organization, error: nil) end end ....
Như bạn thấy, một thay đổi khác từ phiên bản trước của service NewRegistration là chúng ta không còn sử dụng một phương thức .perform và bây giờ sử dụng .call. Sao nó lại quan trọng? Vâng, nó thực sự phổ biến hơn nhiều so với thực hiện, thậm chí là tiêu chuẩn thông thường, mà các nhà bình luận ở đây và những nơi khác đã chỉ ra. Ngoài ra, .call đáp ứng lambdas có thể giúp bạn sử dụng các đối tượng này ở nơi khác.
Chúng ta phân tích các tham số giống như chúng ta đã làm trong phiên bản trước đó, nhưng bây giờ trở thành các biến thông thường thay vì các biến instance khi chúng được chuyển vào các service con mới trong cùng một phương thức public .call. Mặc dù thứ tự và 'waterfall' của các thành phần để thực hiện ở lại tương đối giống nhau trong cả hai phiên bản.
Bây giờ chúng ta có các service dành cho service con riêng lẻ, chúng ta có thể bắt gặp ngoại lệ từ bất kỳ service nào trước khi bắt đầu ... rescue..end block. Bằng cách đó, chúng ta có thể bắt được một vấn đề như tổ chức không lưu, chuyển nó trở lại qua đối tượng cha và controller gọi nó để xử lý ngoại lệ. Thêm vào đó, bây giờ chúng ta đang truyền lại một đối tượng kết quả với OpenStruct cho controller. Đối tượng này sẽ giữ :success? , các đối tượng đã được thông qua để được trả lại, và bất kỳ lỗi nào từ các success.
Với tất cả những thay đổi này, cách thức chúng tôi có thể gọi Service từ controller của chúng tôi hơi khác, nhưng không nhiều:
result = NewRegistration.build.call({user: resource, organization: @org}) if result redirect_to root_path(result.user) else ** redirect_to last_path(result.user), notice: 'Error saving record' end
Cuối cùng, bây giờ là một ví dụ về cách dễ dàng có thể để thử nghiệm service vừa được tái cấu trúc
# spec/services/new_registration_test.rb describe NewRegistration do context "integration tests" do before do @service = NewRegistration.build @valid_params = {user: Factory.create(:user), organization: Factory.build(:organization)} end it "creates an organization in the database" do expect { @service.call(@valid_params) }.to change { Organization.count }.by(1) end ...etc, etc
Ở đó bạn có nó, các đối tượng Registration Service được refactored đã đóng gói Single Responsibility Principle (nó có thể không hoàn hảo đối với đa số mọi người), xử lý kết quả tốt hơn, cải thiện khả năng đọc và xử lý phụ thuộc được cải thiện. Rõ ràng, gần như bất kỳ mã nào liên tục có thể được cải thiện và tôi mong muốn Internet để chọn ngoài phiên bản này của Registration Service.
Bài viết gốc: https://hackernoon.com/going-further-with-service-objects-in-ruby-on-rails-b8aac13a7271