Service Object trong Rails

Service Object trong Rails

Rails tuân theo mô hình MVC (Model-View-Control), điều này đặt ra câu hỏi xung quanh việc lập trình logic sẽ như thế nào khi một ứng dụng Ruby on Rails đạt đến một kích thước nhất định. Một trong những cách để hỗ trợ thiết kế hướng đối tượng một cách đơn giản ngay cả các ứng dụng lớn là dùng Service Object. Trong bài viết này sẽ giúp chúng ta tìm hiểu rõ hơn về Service Object.

1. Thế nào là Service Object?

  • Một Service Object là một PORO (Plain old Ruby Object), có nghĩa là phân tách các đối tượng thành các lớp hoặc mô đun có một phương thức chung, thường được đặt tên là #call và được thiết kế để thực hiện một tác vụ hoặc một dịch vụ. Nó là một cách quản lý logic để các vùng khác của ứng dụng Ruby on Rails không chịu quá nhiều trách nhiệm. Các ứng dụng Rails bị chỉ trích khi có quá nhiều logic được đặt trong Models, Views và Controller - Service Object là một giải pháp.
  • Lấy một ví dụ đơn giản, bạn có các file Controller có số dòng code rất nhiều dẫn đến việc đọc code để tối ưu hoặc fix lỗi sẽ gây rất nhiều khó khăn và tốn thời gian, để tối ưu thì ta cần tạo ra một object để xử lý một chuỗi các logic, do đó thay vì khai báo một đống code trong controller ta chỉ cần một dòng code gọi object đó ra để xử lý, các object đó gọi là Service Object.
  • Tất cả các Service Object sẽ được lưu ở thư mục service thuộc thư mục app mặc định của Rails, thư mục này không có sẵn nên ta có thể tạo bằng câu lệnh:
 $ mkdir app/services

Sau đây ta sẽ tìm hiểu về 2 dạng cấu trúc cơ bản của Service Object là lớp và mô đun

2. Ví dụ Service Object là một lớp

Các lớp là một lựa chọn tốt nếu bạn muốn lưu trữ dữ liệu trong các biến instance và có các phương thức khác để hỗ trợ dịch vụ.
Sau đây là ta sẽ làm một ví dụ Service Object là một lớp. Ta có một file controller như sau

class ExampleController < ApplicationController
  def create
    @posts_name = params[:posts_name]
    @example_text = params[:example_text]
    @example = Example.new(posts: Posts.first(name: @posts_name), text: @example_text)
  end
end

Ví dụ trên chỉ là mẫu đơn giản, trong thực tế file controller rất rắc rối và phức tạp. Giả sử logic trên được sử dụng nhiều lần và ta không muốn làm phình code hơn nữa vì phải viết nhiều lần thì ta có thể đưa nó vào Service để tiện sử dụng. Ta tạo một file service có tên là example_service.rb

 $ touch app/services/example_service.rb

Trong file example_service.rb ta code như sau

class ExampleService
  def initialize(params)
    @posts_name = params[:posts_name]
    @example_text = params[:example_text]
  end
  def call
    posts = Posts.find_by(name: @posts_name)
    Example.new(posts: posts, text: @example_text)
  end
end

Thì file controller ta sẽ viết lại như sau:

class ExampleController < ApplicationController
  def create
    @example = ExampleService.new(params).call
  end
end

Như vậy khối logic trong file controller ban đầu đã được đưa vào services, ở đây dòng code

ExampleService.new(params).call

có tác dụng khởi tạo một service object với thông tin params truyền vào và gọi hàm #call. Bây giờ ta có thể sử dụng khối logic đó ở bất kì đâu bằng cách gọi service object. Rất thuận tiện phải không nào! Ngoài việc làm lớp, Service Object có thể là một mô đun

3. Ví dụ Service Object là một mô đun

Nếu bạn không muốn lưu trữ bất kỳ dữ liệu nào dưới dạng các biến instance thì mô-đun có thể là một tùy chọn nhẹ hơn.
Sau đây là ta sẽ làm một ví dụ Service Object là một mô đun. Với file controller tương tự như ví dụ trên thì trong file example_service.rb ta code như sau

module ExampleModuleService
  class << self
    def create(params)
      posts_name = params[:posts_name]
      example_text = params[:example_text]
      posts = Posts.find_by(name: posts_name)
      Example.new(posts: posts, text: example_text)
    end
  end
end

Nó cũng làm cho bộ điều khiển mã code dễ quản lý hơn rất nhiều. Và trong file controller ta sẽ code như sau

class ExampleController < ApplicationController
  def create
    @example = ExampleModuleService.create(params)
  end
end

Nếu bạn bắt đầu sử dụng ngày càng nhiều Service Objects, bạn có thể thấy thư mục service của mình được mở rộng rất nhiều. Do đó, bạn có thể quản lý sự tăng trưởng này bằng cách tổ chức chúng vào các thư mục và mô-đun.

4. Quản lý nhiều Service Objects bằng mô đun

Khi bạn đã hiểu các nguyên tắc trong Service Objects thì thật dễ dàng sử dụng nó và bạn sẽ sử dụng thật nhiều. Và hệ quả là thư mục service bị phình to. Để khắc phục điều này ta có thể sắp xếp các service thành các nhóm bằng các mô đun. Như ví dụ ở trên, ta có thể nhóm như sau:

module Example
  module Build
    def self.call(params)
      posts_name = params[:posts_name]
      example_text = params[:example_text]
      posts = Posts.find_by(name: posts_name)
      Example.new(posts: posts, text: example_text)
    end
  end
end

Sau đó đặt nó vào một đường dẫn thư mục để phản ánh cấu trúc mô-đun, nếu không, Rails sẽ tự động tải nó. Bạn nên đơn giản hóa việc đặt tên như sau

services/example/build.rb
services/example/destroy.rb
services/posts/build.rb
...

Điều này có thể làm cho việc sử dụng Service Objects có thể mở rộng khi ứng dụng của bạn tăng kích thước.

5. Trích xuất code từ Models, Views, Controllers đến Service Objects

Để đảm bảo rằng trích xuất mã vào Service Objects mới từ các vùng khác trong ứng dụng của bạn mà không phá vỡ chức năng hiện có, dưới đây là cách tốt nhất để phát triển phần mềm theo cách này dựa trên thử nghiệm.

Testing

Hy vọng, bạn sẽ có các bài kiểm tra đơn vị hiện tại cho code của mình để bạn có thể chuyển sang tệp kiểm tra cho Service Objects mới của mình. Nếu không , bạn cần đảm bảo rằng tất cả các vùng trong lớp hoặc mô đun mới đều được kiểm tra. Điều quan trọng nữa là phải có các thử nghiệm tích hợp để xem sự thay đổi sẽ tác động đến các vùng khác trong ứng dụng của bạn như thế nào khi Service Objects của bạn được gọi.

Trích xuất code đến Service Objects

Điều này sẽ thể hiện việc trích xuất code từ Controllers đến Service Objects, nguyên tắc tương tự có thể áp dụng cho các phần khác trong ứng dụng Ruby on Rails.

  • Trong file controller gốc
class ExampleController < ApplicationController
  def create
    posts_name = params[:posts_name]
    example_text = params[:example_text]
    posts = Posts.find_by(name: posts_name)
    @example = Example.new(posts: posts, text: example_text)
  end
end

Nội dung cần trích dẫn từ file controller gốc

posts_name = params[:posts_name]
example_text = params[:example_text]
posts = Posts.find_by(name: posts_name)
@example = Example.new(posts: posts, text: example_text)
  • Code được trích xuất vào một Service Object
module ExampleModule
  module Builder
    def self.call(params)
      posts_name = params[:posts_name]
      example_text = params[:example_text]
      posts = Posts.find_by(name: posts_name)
      Example.new(posts: posts, text: example_text)
    end
  end
end

Nội dung được trích dẫn vào một Service Object

posts_name = params[:posts_name]
example_text = params[:example_text]
posts = Posts.find_by(name: posts_name)
Example.new(posts: posts, text: example_text)
  • Một ví dụ về một controller được cấu trúc lại bằng cách sử dụng Service Object
class ExampleController < ApplicationController
  def create
    @example = Services::ExampleModule::Builder.call(params)
  end
end

Dòng lệnh để gọi Service Object ở ví dụ trên

Services::ExampleModule::Builder.call(params)

Bây giờ bạn đã sẵn sàng tạo Service Object, bạn có thể sử dụng chúng để dọn sạch code của mình bằng guard clauses.

  • Guard clauses

Guard clauses là một cách viết code bảo vệ dòng logic tiếp tục nếu một số điều kiện được đáp ứng hoặc không được đáp ứng.

return ACTION if CONDITION

Service Objects có thể được sử dụng để thiết kế hoặc cấu trúc lại code để logic được tránh khỏi các phần khác của ứng dụng Ruby on Rails, chẳng hạn như controller.

Trong ví dụ trên, bạn có thể thấy cách quản lý user, bản ghi Exampleview chế độ xem được xử lý bên ngoài controller bởi Service Objects. Điều này cho thấy chúng có thể được sử dụng cho cả hai mục đích ACTIONCONDITION trong guard clauses.

class ExampleController < ApplicationController
  def create
    return redirect_to(:root) unless 
      Services::User::Validator.call(params)
    @example = Services::Example::Builder.call(params)
    return Services::Example::ViewRenderer.call if @example.save!
    redirect_to(:root)
  end
end

6. Kết luận

Tóm lại, mình có các lợi ích khi sử dụng Service Objects là giúp cho code ngắn gọn dễ đọc hơn nhiều, các khối logic được tách ra quản lí một cách dễ dàng, khả năng sửa lỗi và tái sử dụng tối ưu hơn. Hi vọng, qua các ví dụ trên thì các bạn đã dần hiểu được Service Objects và tác dụng của nó, cách sử dụng cũng khá là đơn giản và mang lại hiệu quả rất nhiều. Service Objects đơn giản và mạnh mẽ, hãy tìm hiểu và sử dụng chúng nhé!
Tham khảo: