Service object là Ruby object thực hiện một hành động đơn lẻ. Nó encapsulates một process trong domain hoặc business logic của bạn. Tưởng tượng bạn cần tạo book instance trong ứng dụng thư viện tưởng tượng; trong Rails app thường, bạn làm:
class BookController < ApplicationController
def create
Book.new(*args)
end
end
Điều này ổn cho việc đơn giản. Tuy nhiên, khi app lớn, bạn có thể end up với nhiều boilerplate bao quanh nó:
class BookController < ApplicationController
def create
default_args = { genre: find_genre(), author: find_author() }
Book.new(attrs.merge(default_args))
end
private
def find_genre
// ...
end
def find_author
// ...
end
end
Service objects cho phép abstract behavior này vào class riêng. Code trở nên đơn giản lại:
class BookController < ApplicationController
def create
BookCreator.create_book
end
end
Tại Sao Cần Service Objects
Rails thiết kế native support MVC (models, controllers, views, helpers). Cấu trúc này đủ cho app đơn giản. Tuy nhiên, khi app lớn, bạn bắt đầu thấy domain/business logic rải rác khắp models và controller. Logic này không thuộc controller hay model, làm code khó reuse và maintain. Rails service object là pattern giúp tách business logic khỏi controllers và models, cho models làm data layers và controller entry point API.
Chúng ta nhận nhiều benefit khi introduce services encapsulate business logic:
Lean Rails controller - Controller chỉ hiểu request và chuyển request params, sessions, cookies thành arguments truyền vào service object xử lý. Controller redirect hoặc render theo service response. Dù app lớn, controller actions dùng service objects thường không quá 10 lines code.
Testable controllers - Vì controller lean và làm collaborator với service, test dễ dàng, chỉ check certain methods trong controller được called khi action xảy ra.
Test business process in isolation - Services dễ và nhanh test vì là small Ruby objects tách khỏi environment. Dễ stub collaborators và chỉ check certain steps trong service.
Re-usable services - Service objects gọi được bởi controllers, service objects khác, queued jobs, v.v.
Separation giữa framework và business domain - Rails controllers chỉ thấy services và interact domain object dùng chúng. Giảm coupling làm scalability dễ dàng, đặc biệt khi chuyển từ monolith sang microservice. Services dễ extract và move sang service mới với minimal modification.
Tạo Service Object
Trước hết, tạo BookCreator mới trong thư mục app/services cho imaginary library management application:
$ mkdir app/services && touch app/services/book_creator.rb
Tiếp, dump logic vào Ruby class mới:
# app/services/book_creator.rb
class BookCreator
def initialize(title:, description:, author_id:, genre_id:)
@title = title
@description = description
@author_id = author_id
@genre_id = genre_id
end
def create_book
Boook.create!(
title: @title
description: @description
author_id: @author_id
genre_id: @genre_id
)
rescue ActiveRecord::RecordNotUnique => e
# handle duplicate entry
end
end
Sau đó, gọi service object trong controller hoặc bất kỳ đâu trong app:
class BookController < ApplicationController
def create
BookCreator.new(title: params[:title], description: params[:description], author_id: params[:author_id], genre_id: params[:genre_id]).create_book
end
end
Service Object Syntactic Sugar
Chúng ta đơn giản chuỗi BookCreator.new(arguments).create bằng class method khởi tạo BookCreator và gọi create method:
# app/services/book_creator.rb
class BookCreator
def initialize(title:, description:, author_id:, genre_id:)
@title = title
@description = description
@author_id = author_id
@genre_id = genre_id
end
def call(*args)
new(*args).create_book
end
private
def create_book
Boook.create!(
title: @title
description: @description
author_id: @author_id
genre_id: @genre_id
)
rescue ActiveRecord::RecordNotUnique => e
# handle duplicate entry
end
end
Trong controller, book creator giờ gọi:
class BookController < ApplicationController
def create
BookCreator.call(
title: params[:title],
description: params[:description],
author_id: params[:author_id],
genre_id: params[:genre_id]
)
end
end
Để keep code DRY và reuse behavior với service objects khác, abstract call method vào base ApplicationService class mà mọi service object inherit:
class ApplicationService
def self.call(*args)
new(*args).call
end
end
Với code này, refactor BookCreator inherit ApplicationService:
# app/services/book_creator.rb
class BookCreator < ApplicationService
def initialize(title:, description:, author_id:, genre_id:)
@title = title
@description = description
@author_id = author_id
@genre_id = genre_id
end
def call
create_book
end
private
def create_book
# ...
end
end
Tạo Service Objects Dùng BusinessProcess Gem
Với BusinessProcess gem, không cần tạo base application service class hoặc define initialize method vì gem có sẵn config. Service object chỉ inherit BusinessProcess::Base.
Trong Gemfile thêm:
gem 'business_process'
Chạy bundle trong terminal
class BookCreator < BusinessProcess::Base
# Specify requirements
needs :title
needs :description
needs :author_id
needs :genre_id
# Specify process (action)
def call
create_book
end
private
def create_book
# ...
end
end
Hướng Dẫn Tạo Good Service Objects
One public method
Service object nên perform one business action và làm tốt, nên chỉ expose single public method. Methods khác private và called by public method. Bạn chọn tên public method tùy thích, miễn consistent across all service objects. Ví dụ đặt call. Tên conventional khác: perform, execute.
Đặt Tên Service Objects Theo Role Chúng Thực Hiện
Tên service object nên chỉ rõ nó làm gì. Cách phổ biến đặt tên từ kết thúc "or" và "er". Ví dụ, job tạo book đặt BookCreator, job đọc book đặt BookReader.
Không Khởi Tạo Service Objects Trực Tiếp
Dùng abstractions như syntactic sugar pattern hoặc gems như BusinessProcess để rút gọn notation gọi service objects. Cách này cho phép đơn giản BookCreator.new(*args).call hoặc BookCreator.new.call(*args) thành BookCreator.call(*args), ngắn gọn và readable hơn.
Nhóm Service Objects Trong Namespaces
Introduce service objects, đặc biệt app lớn, nghĩa là grow từ one service object đến hàng chục. Để improve code organization, good practice nhóm common service objects vào namespaces. Trong library app, group all book-related services together, author-related services separate namespace. Folder structure:
services
├── application_service.rb
└── book
├── book_creator.rb
└── book_reader.rb
Service objects trông như:
# services/book/book_creator.rb
module Book
class BookCreator < ApplicationService
...
end
end
# services/book/book_reader.rb
module Book
class BookReader < ApplicationService
...
end
end
Gọi thành Book::BookCreator.call(*args) and Book::BookReader.call(*args).
One responsibility per service object
Service object làm hơn một việc đi ngược "business action" mindset. Conventionally, generic service object perform multiple actions discouraged. Muốn share code giữa service objects, tạo base hoặc helper module dùng mixins include vào service objects.
Rescue Exceptions Và Raise Custom Exceptions
Mục đích service object encapsulate implementation details bên trong, như interactions third-party services/libraries hoặc database manipulation Rails ActiveRecord. Nếu error xảy ra, như ActiveRecord::RecordNotUnique, khi interact ActiveRecord, service cần rescue exception properly. Errors không nên propagate lên call stack. Nếu không handle trong rescue block, raise custom-defined exception specific to service object.
Kết Luận
Service object pattern có thể greatly improve overall design ứng dụng khi bạn thêm features. Nó làm codebase expressive hơn, dễ maintain, và ít đau đớn khi test.
Service object pattern có thể greatly improve overall design ứng dụng khi bạn thêm features. Nó làm codebase expressive hơn, dễ maintain, và ít đau đớn khi test.