Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Chuyển Đổi Ứng Dụng Rails Dùng Service Objects: Kiến Trúc Sạch, Scalable

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.