12/08/2018, 13:28

Sử dụng Service Object trong Rails giúp bảo trì code

Nếu bạn đi theo hướng Ruby on Rails, bạn sẽ nghe thấy nhiều từ 'service' hoặc thậm chí còn gặp nó trong thư mục app/services. Service Objects Service Object thực hiện tương tác của user với ứng dụng. Nó chứa business logic điều phối các thành phần tạo tác khác. Thật ra khi nhìn vào thư mục ...

Nếu bạn đi theo hướng Ruby on Rails, bạn sẽ nghe thấy nhiều từ 'service' hoặc thậm chí còn gặp nó trong thư mục app/services.

Service Objects

Service Object thực hiện tương tác của user với ứng dụng. Nó chứa business logic điều phối các thành phần tạo tác khác.

Thật ra khi nhìn vào thư mục services của một ứng dụng có thể cho người lập trình biết những chức năng thật sự của ứng dụng thực hiện mà thường không rõ ràng khi chúng ta xem các controllers và các models.

Để hiểu rõ hơn sẽ có một ví dụ về ứng dụng Rails sử dụng services.

  • app/

    • services/
      • create_invoice.rb
      • correct_invoice.rb
      • pay_invoice.rb
      • register_user.rb
      • register_user_with_google.rb
      • change_password.rb

Chúng ta thấy rằng đây là ứng dụng hóa đơn chỉ nhìn vào mô hình User và hóa đơn nhưng khi nhìn vào service cho chúng ta biết về mục đích chính tương tác người dùng với ứng dụng.

Ứng dụng này cho phép ngườni dùng tạo hóa đơn, chính sửa và thành toán. Ngoài ra còn có chức năng đăng ký với tài khoản Google và chức năng đổi mật khẩu.

Lợi ích Services là tập trung lõi logic của ứng dụng vào trong object riêng biệt thay vì phân tán nó vào các controllers và các models. Dưới đây cho biết cách làm sử dụng và làm việc với service.

Quá trình thực hiện

  • Nhận Input
  • Thực hiện các chức năng
  • Trả về kết quả

Khởi tạo

Một service thực thi tương tác của user vậy nó khởi tạo với đối tượng user đó. Trong ứng dụng web, service là user thực hiện yêu cầu. Ngoài ra khởi tạo biến còn có thêm các khung cảnh nếu áp dụng được.

Input

Service object nhận input của user có thể là form submit hoặc dữ liệu dạng JSON. Trong code của ứng dụng input có thể lấy nhiều forms.

Single value - một trường ít nhìn thấy.

Hash of values - ví dụ các params một controller rails phổ biến là dễ sử dụng. Tuy nhiên có hạn chế binding service với định dạng đầu vào là khi định dạng input thay đổi serivice bên trong phải thay đổi theo.

Form Object một đối tượng riêng biệt thể hiện input của user. Nó xử lý phân tích, xác định đầu định dạng đầu vào, giải phóng service. Nó rất hữu ích để tách phân tích cú pháp các params phức tạp từ công việc thực sự thực hiện trong service.

Ví dụ: InvoiceForm có thể biến đối 3 trường riêng biệt vào một Time object để làm cho tiện khi làm việc với code ứng dụng.

#form_object.rb
class InvoiceForm
attr_reader :params

  def initialize params
    @params = params
  end

  def billing_date
    Time.new(params[:year], params[:month], params[:day]) if time_data_present?
    rescue ArgumentError
  end

  def company_name
    params[:_messed_up_company_name] || params[:company_name]
  end
  def valid?
    billing_date.present? && company_name.present?
  end

  private
  def time_data_present?
      [params[:year], params[:month], params[:day]].all?(&:present?)
  end
end

form = InvoiceForm.new({year: "2014", month: "07", day: "18"})`

form.billing_date # => Time instance

Sử dụng

Đặc điểm của service là khi chúng ta gọi một service nó thực hiện gửi một thông điệp gọi đến serivce instance. Phương thức gọi sử dụng data đạt vào khởi tạo và input data để thực hiện công việc của service. Nó bao gồm :

  • creating/ updating/ deleting một record
  • điều phối việc tạo / cập nhật nhiều record
  • phân cấp cho các dịch vụ khác cho việc gửi emails, gửi thông báo

Cuối cùng phương thức sẽ trả về kết quả.

Kết quả

Service Object có thể thự hiện các thao tác. Đối với người lập trình biết rằng sẽ có thứ nào đó thực hiện sai vì vậy cần phải có thông báo khi thành công hoặc có lỗi xảy ra khi sử dụng service. Có 4 cách để kiểm tra : Boolean value cách đơn giản nhất chỉ trả về true khi thành công và false khi có lỗi. Nó chỉ trả về giá trị nhưng không thể mang thông tin nào khác được.

ActiveRecord Object nếu vài trò của service là tạo mới hoặc cập nhật rails models, kết quả là nó trả về một object. Chúng ta có thể kiểm tra của lỗi trong biến trả về để xác định cách gọi service thành công. Status Object chúng ta dùng các đối tượng dụng tiện ích để báo hiệu thành công hay lỗi nó giúp trong nhiều trường hợp phức tạp.

#status_object.rb
class Success
attr_reader :data
  def initialize(data)
    @data = data
  end
  def success?
    true
  end
end

class Error
attr_reader :error
  def initialize(error)
    @error = error
  end
  def success?
    false
  end
end

class AuthorizationError < Error
attr_reader :requesting_user
  def initialize(requesting_user, requested_clearance)
    @requesting_user = requesting_user
    @requested_clearance = requested_clearance
    super("User #{requesting_user.id} does not have required clearance level #{requested_clearance}")
  end
end

AuthorizationError.new(current_user, :admin)

Nêu lên ngoại lệ chúng ta sẽ nêu lên ngoại lệ với bật kỳ kiểu lỗi trong đối tượng service. Giống như các đối tượng status thì các ngoại lện có thể chứa dữ liệu và có nghĩa đầy đủ trong domain.

#exceptions.rb
class MyAppError < StandardError
attr_reader :data
  def initialize(data)
    super(data)
    @data = data
  end
end

class AuthorizationError < MyAppError
  def requesting_user
    data
  end
end

raise AuthorizationError, current_user

Điều hiển nhiên là khi gọi hoàn thành mà không có ngoại lệ coi như thành công. chúng ta nên khởi tạo service với dependencies như input. Nó cho phép trích xuất phương thức private trong service mà không cần phải nhận input là đối số. Chúng ta có thể thực hiện theo cấu trúc sau

#service_patterns.rb
class ServiceObject
attr_reader :input
# ...
  private
  def private_method_with_input
    input.field # access input as method
  end
end

ServiceObject.new(user, dependency, input).call

# Other ways:
ServiceObject.new(user, dependency).call(input)
ServiceObject.call(user, dependency, input)

Sử dụng Services Như chúng ta đã thấy cách đối tượng service có thể thực hiện bây giờ chúng ta sẽ áp dụng vào code của ứng dụng. Trong khung cảnh Rails thì controller là danh giới giữa giao diện người dùng và ứng dụng. Một ứng dụng sử dụng các serviecs có thể khởi tạo trong các controller actions, chỉ cho nó làm việc và phản ứng lại đến user.

#invoices_controller.rb
class InvoicesController < ApplicationController
# ...

  def create
    form = InvoiceForm.new(params)
    result = CreateInvoice.new(current_user, form).call
    @invoice = result.invoice
    if result.success?
      redirect_to @invoice
    else
      render :edit, error: result.error
  end
end
# ...
end

Sử dụng các controllers với các services làm cho controller thật nhẹ vì tất cả business logic được đóng gói trong serivces và models và phân tích input trong đối tượng form.

Đây là một API cho phép dùng Service objects, Status objects và Rails Responder.

#responder.rb
class APIResponder < ActionController::Responder
  private
  def display(resource, options = {})
    super(resource.data, options)
  end
  def has_errors?
    !resource.success?
  end
  def json_resource_errors
    { error: resource.error, message: resource.error_message, code: resource.code, details: resource.details }
  end
  def api_location
   nil
  end
end

class Success
attr_reader :data
  def initialize(data)
    @data = data
  end
  def success?
    true
  end
end

class Error
attr_reader :error, :code, :details
  def initialize(error = nil, code = nil, details = nil)
    @error = error
    @code = code
    @details = details
  end
  def error_message
    error.to_s
  end
  def success?
    false
  end
end

class ValidationError < Error
  def initialize(details)
    super(:validation_failed, 101, details)
  end
  def error_message
    "Validation failed: see details"
  end
end

class InvoicesController < ApplicationController
  respond_to :json
  def create
    form = InvoiceForm.new(params)
    result = CreateInvoice.new(current_user, form).call # => ValidationError.new(invoice.errors)
    respond_with result # { error: "validation_error", code: 101, message: "..." details: { ... } }
  end
  def update
    result = UpdateInvoice.new(current_user, params[:id]).call # => Success.new(invoice)
    respond_with(result) # { billing_date: ..., company_name: ... }
  end
# ...
  def self.responder
    APIResponder
  end
end

Kết luận

Trong bài viết này chủ yếu vào Rails là một dependency có thể mô tả các service object. Vì chúng ta có thể dùng service object với bật kỳ web framework khác như mobile hoặc ứng console. Bài viết này nhằm mục đích giúp cho biết cách sử dụng các service objects giúp cho việc bảo trì code.

0