ActiveRecord refactoring (P2) - Services
Bài đăng này đã không được cập nhật trong 3 năm
Mở đầu
Như mình đã nói đến trong bài viết ActiveRecord refactoring (P1), tiếp sau concerns
thì hôm nay mình sẽ tiếp tục với services
trong Ruby.
Xin tiếp tục dịch bài viết ActiveRecord Refactoring của tác giả Luke Morton.
Phần 2. Services
Services
- hay còn được gọi là Interactors
, là một mô hình của Rails với mục đích là giữ cho logic trong một controller
được rõ ràng, súc tích mà vẫn không làm mất đi trách nhiệm điều khiển của nó.
Một lý do chính khác để sử dụng mô hình này là sẽ giúp tăng số lượng dòng code có thể test độc lập mà không cần phải khởi động lại Rails.
Như ta đã biết, controller
là cầu nối giữa các phần còn lại trong ứng dụng của bạn.
Trong Rails, một controller
thường là một interface
cho các thao tác CRUD
của một model ActiveRecord
. Điều này liên quan đến việc xử lý các parameter HTTP
của một request
và sử dụng chúng để thay đổi dữ liệu của ứng dụng, ví dụ như cập nhật hồ sơ của người dùng.
hực hiện một thay đổi, controller
phải đảm bảo rằng model
sẽ hài lòng với cập nhật được thực hiện bằng cách kiểm tra tính hợp lệ của dữ liệu. Sau đó nó phải trả về lỗi cho người dùng nếu như có lỗi xảy ra (có thể là thiếu một trường bắt buộc nào đó) để người dùng có thể khắc phục nó. Ngay khi cập nhật thành công thì controller
sẽ chuyển hướng và đưa ra một thông báo thành công.
controller
cũng được sử dụng để gửi mail bằng cách sử dụng class ActionMailer
. Thường thì điều này chỉ xảy ra sau một số trường hợp ví dụ như gửi email chào mừng sau khi đăng ký tài khoản thành công.
Đây chỉ là một trong những công việc của controller
trong các ứng dụng tôi đã làm việc. Tôi chắc rằng có rất nhiều công việc khác mà mọi người đang sử dụng.
Bạn có lẽ sẽ suy nghĩ rằng không biết là điều này để làm gì với refactoring ActiveRecord
? Hãy xem xét hai ví dụ minh họa cho điều này.
Ví dụ đầu tiên cho thấy sự cần thiết tạo mới Artist
khi đang tạo mới Event
.
class Event < ActiveRecord::Base
has_and_belongs_to_many :artists
accepts_nested_attributes_for :artists
end
class Artist < ActiveRecord::Base
has_and_belongs_to_many :events
end
ActiveRecord
xử lý hầu hết logic cho các mối quan hệ, bạn rất có thể sẽ gọi đến Event#create
trong hàm create
của controller
như sau :
class EventsController < ApplicationController
def create
@event = Event.create(params[:event])
if @event.valid?
flash[:success] = "Saved"
redirect_to events_path
else
render :new
end
end
end
Bây giờ, thực hiện việc gửi email cho mỗi artist
để báo với họ vừa được thêm vào một event
. Thường thường thì tôi thấy công việc này được đặt trong model
.
class Events < ActiveRecord::Base
has_and_belongs_to_many :events
after_create :notify_artists
def notify_artists
artists.each do |artist|
EventMailer.notify_artist(self, artist).deliver_later
end
end
end
controller
vẫn được giữ nguyên. Cũng không tệ nhỉ? Nhưng sẽ làm thế nào nếu sau đó bạn muốn thêm một delayed job
cho từng artist
như sau :
class Events < ActiveRecord::Base
# ...
after_create :poll_soundcloud
def poll_soundcloud
artists.each do |artist|
ArtistSoundcloudJob.perform_later(artist)
end
end
end
Bây giờ, model Even
đã có method để gửi email chào đón tới các artist
mới và các công việc được thực hiện sau đó. Ngay từ cái nhìn đầu tiên điều này có vẻ không quá khó giải quyết. Khi bạn test model Event
tuy nhiên bây giờ bạn cần stub
hoặc mock
lời gọi ra EventMailer
và ArtistSoundcloudJob
. Giống như là khi ta thêm nhiều email và công việc hơn.
Bạn có thể khắc phục bằng cách giữ logic ở ngoài tầng dữ liệu của bạn và thay thế vào đó là gọi đến controller
.
class EventsController < ApplicationController
def create
@event = Event.create(params[:event])
if @event.valid?
notify_artists
poll_soundcloud
flash[:success] = "Saved"
redirect_to events_path
else
render :new
end
end
def notify_artists
@event.artists.each do |artist|
EventMailer.notify_artist(@event, artist).deliver
end
end
def poll_soundcloud
@event.artists.each do |artist|
ArtistSoundcloudJob.perform_later(artist)
end
end
end
Logic bây giờ được tổ chức bởi controller
. Tuy nhiên, nó có thể làm cho controller
phình to ra. Không chỉ có vậy, chúng ta có thể thêm email và công việc cho các thao tác CRUD
khác, ví dụ như gửi email cho artist
khi mà event
bị hủy chẳng hạn. Điều này là rất thực tiễn.
Đi vào lớp service
. Thông thường, service
sẽ xử lý một hành động CRUD
. Bạn có thể có các class như là EventCreate
, EventUpdate
và EventDelete
cho model Event
. Hãy di chuyển hai phương thức notify_artists
và poll_soundcloud
vào trong một service
như sau :
class EventCreate
def exec(attrs)
event = Event.create(attrs)
if event.valid?
notify_artists
poll_soundcloud
end
event
end
def notify_artists
@event.artists.each do |artist|
EventMailer.notify_artist(@event, artist).deliver
end
end
def poll_soundcloud
@event.artists.each do |artist|
ArtistSoundcloudJob.perform_later(artist)
end
end
end
Ta cập nhật lại controller
thành :
class EventsController < ApplicationController
def create
@event = EventCreate.new.exec(params[:event])
if @event.valid?
flash[:success] = "Saved"
redirect_to events_path
else
render :new
end
end
end
controller
bây giờ trông rất giống như lúc ban đầu, chỉ khác là ở đây biến instance @event
được khai báo bằng EventCreate.new.exec(params[:event])
thay cho Event.create(params[:event])
. Khác biệt là thực tế, không phải controller
hay là model
nào cũng biết về việc gửi mail và hàng chờ các công việc cần làm. Và chúng ta đã đạt được một số lợi ích từ việc này.
Chúng ta có thể tạo service
cho mỗi thao tác CRUD
và giữ độ dài của các class ở mức thấp, nên các class sẽ dễ đọc hiểu và duy trì hơn.
Khi là class nhỏ hơn thì test đơn vị cũng có thể nhỏ hơn. Có ít mock
khi test controller và model
hơn. Trong controller
, bạn có thể stub
hoặc là mock
EventCreate
hoàn toàn. Trong model
, bạn không cần phải stub
hay là mock
bất kỳ cái gì cả. Trong service
, bạn có thể stub
hoặc mock
model, mailer và hàng đợi công việc.
Tham khảo
Bài viết liên quan
Cảm ơn bạn đã theo dõi bài viết.
tribeo
All rights reserved