12/08/2018, 13:20

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.

using-services.png

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

0