Sử dụng Service Object trong Rails giúp bảo trì code
Bài đăng này đã không được cập nhật trong 8 năm
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.
Tài liệu tham khảo Service objects
All rights reserved