Viblo Learning
-2

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 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


All Rights Reserved