ActiveRecord refactoring (P2) - Services
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ở đầ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
- ActiveRecord Refactoring
- Slow database test fallacy
- ActiveRecord refactoring (P1) - Concerns
- ActiveRecord refactoring (P3) - Presenters
Cảm ơn bạn đã theo dõi bài viết.
tribeo