DEV Community

Cover image for FormService: a PORO ServiceObjects with a state
Povilas Jurčys
Povilas Jurčys

Posted on • Updated on

FormService: a PORO ServiceObjects with a state

The ServiceObject pattern is a powerful way to encapsulate specific business actions or workflows in Ruby on Rails. By separating concerns and keeping your controllers lean, ServiceObjects can help make your codebase more maintainable and easier to understand.

The example provided, CreateUser, is a simple ServiceObject that takes in user parameters, creates a new user, and sends a welcome email. The call method is the main entry point for the ServiceObject and should contain all the logic needed to perform the desired action.

Here's an example of the CreateUser service class:

class CreateUser
  def self.call(...)
    new(...).call
  end

  def initialize(user_params:)
    @user_params = user_params
  end

  def call
    user = User.create!(@user_params)
    UserMailer.welcome_email(user: user).deliver_now
    user
  end
end
Enter fullscreen mode Exit fullscreen mode

To use this CreateUser service in your controller, you can call the call method on the class and pass in the necessary parameters.

class UsersController < ApplicationController
  def create
    user = CreateUser.call(user_params: params[:user])
    redirect_to user_path(user)
  end

  private

  def user_params
    params.require(:user).permit(:name, :email, :password)
  end
end
Enter fullscreen mode Exit fullscreen mode

ServiceObjects and validation

When using the ServiceObject pattern in Ruby on Rails, it's important to consider how to handle validations. Validations are a crucial part of any application and are used to ensure that the data being passed to the ServiceObject is in the correct format and meets certain requirements.

This is where FormObjects shine!

In my Using FormObject pattern in Ruby APIs post I already talked what's so great about FormObjects. In short, the FormObject pattern is a way to encapsulate form logic and validation in a separate object. It can be used in conjunction with the ServiceObject pattern to separate validation and the actual action.

Here's an example of how you could use a FormObject and ServiceObject together in a controller:

class UsersController < ApplicationController
  def create
    form = CreateUserForm.new(user_params)
    if form.valid?
      user = CreateUser.call(form.user_params)
      redirect_to user_path(user)
    else
      render :new
    end
  end

  private

  def user_params
    params.require(:user).permit(:name, :email, :password)
  end
end
Enter fullscreen mode Exit fullscreen mode

In this example, the controller creates a new instance of the CreateUserForm FormObject and passes in the user_params. The FormObject is responsible for validating the user_params and returning a boolean value indicating if the form is valid.

If the form is valid, the controller calls the CreateUser.call method and passing the form.service_params. This is the ServiceObject responsible for creating the user and sending a welcome email.

If the form is not valid, the controller will render the new template and display the errors from the FormObject.

Here is an example of how the CreateUserForm FormObject could look like:

class CreateUserForm
  include ActiveModel::Model

  attr_accessor :name, :email, :password

  validates :name, :email, :password, presence: true
  validates :email, format: { with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i }

  def service_params
    { name: name, email: email, password: password }
  end
end
Enter fullscreen mode Exit fullscreen mode

In this example, the FormObject includes the ActiveModel::Model module, which provides basic form functionality, such as validation and attribute accessors. The FormObject defines the attributes name, email, password and uses Rails built-in validation methods validates to check for the presence and format of the attributes.

The service_params method returns a hash of the attributes that will be passed to the ServiceObject.

By using the FormObject and ServiceObject pattern, you can keep your controllers lean and focused on their main responsibility, which is to handle the flow of the application. The FormObject is responsible for validating the data and the ServiceObject is responsible for performing the actual action.

This approach helps to separate concerns, making your codebase more maintainable and easier to test.

FormService: one step further

When working with ServiceObjects in Ruby on Rails, it can be useful to have a way to track the state of the object, including whether or not the action was successful, the result of the action, and any errors that occurred. One way to achieve this is by using a FormService, which combines the functionality of a FormObject and a ServiceObject, and returns a state object that contains this information.

Here is how FormService might look like:

class FormService
  def initialize(form_instance, service_class)
    @form_instance = form_instance
    @service_class = service_class
  end

  def call
    FormService::State.new(
      form: @form, service_class: @service_class
    )
  end
end
Enter fullscreen mode Exit fullscreen mode

This class is just a syntactic sugar which makes it look like a service. Real magic happens in FormService::State object. The FormService::State class is responsible for holding the state of the object and providing access to the result, success and errors after the FormService is called:

class FormService::State
  NO_ERRORS = ActiveModel::Errors.new(nil)

  def initialize(form:, service_class:)
    @form = form
    @service_class = service_class
  end

  def success?
    errors.empty?
  end

  def result
    return nil unless success?
    result!
  end

  def errors
    @errors ||= @form.invalid? ? @form.errors : NO_ERRORS
  end

  private

  def result!
    return @result if defined?(@result)
    @result = @service_class.call(@form.service_params)
  end
end
Enter fullscreen mode Exit fullscreen mode

Here's an example of how you could use the FormService in a controller:

def create
  user_form = UserForm.new(user_params)
  outcome = FormService.new(user_form, CreateUser).call
  if outcome.success?
    redirect_to user_path(outcome.result)
  else
    @errors = outcome.errors.full_messages
    render :new
  end
end
Enter fullscreen mode Exit fullscreen mode

In this example, the controller creates a new instance of the UserForm FormObject and passes in the user_params. The FormService is initialized with the UserForm form object instance and the CreateUser service class. The call method is called on the FormService and it returns a FormService::State object.

You can use the outcome object returned by the FormService to handle the flow of the application. In the example above, if the outcome is successful, the controller redirects the user to the appropriate page, otherwise, it renders the new template and displays the errors.

This approach allows you to separate validation and the actual action, making your codebase more maintainable, and your application more predictable. You can also make it more robust by handling different types of errors or adding more methods to the FormService::State class.

Final thoughts

One of the advantages of using the FormService is that it allows you to keep your ServiceObject stateless. A stateless object is an object that does not maintain any information about its previous interactions and can be reused without any side-effects. This can be beneficial in a number of ways and can help to avoid an extra layer of complexity in your application.

When using the ServiceObject pattern, it's common to pass in the data to be used for the action as an argument to the call method. The ServiceObject then performs the action using the data and returns the result. This approach can work well for simple actions, but as the complexity of the application increases, it can be challenging to manage the state of the ServiceObject.

By contrast, the FormService allows data validation and the action responsibilities to be separated. The FormObject is responsible for validating the data, and the ServiceObject is responsible for performing the action. The FormService then calls the call method on the ServiceObject, passing in the validated data.

Additionally, by keeping the ServiceObject stateless, you can easily reuse the ServiceObject for different use cases, and also you can use it in a variety of contexts without any changes.

Top comments (4)

Collapse
 
writerzephos profile image
Bryant Morrill

I love the creativity here. However I feel like these solutions are very verbose and there are cleaner/more elegant solutions already available. That's not a criticism! I only just learned about Dry Transactions myself, and they give me all the features your exampled provide, but with a much more concise DSL.

On top of all that, the Dry Transaction library supports wrapping your whole process in database transactions and is extensible using custom step adapters. It really is a fantastic library.

Check it out here: dry-rb.org/gems/dry-transaction/0....

Collapse
 
povilasjurcys profile image
Povilas Jurčys

@writerzephos , @thadeu thank you for showing me those libraries! ♥️ I need to understand how much each tool benefits compared with a bit more verbose, but a no-dependencies-based solution. I felt that my article has already quite complex code, so I skipped the part on how to make code short. In case you are curious, here is how it can be used it in our controllers with some helper methods:

# controllers/api/users_controller.rb
def create
  form = UserCreateForm.new(user_params)
  # block is called only on successful case
  respond_with_form_service(user_create_form) { |user| render(json: user) }
end
Enter fullscreen mode Exit fullscreen mode

I really appreciate that you shared your libraries - I learned a lot. Seems like those libraries add a lot of additional goodies and allow to have somewhat-like Organizers or better control of the multi-service flow (even though I am worried if it does not complicate testing and debugging). I think those libs might be a good inspiration for my next blog post!

Collapse
 
writerzephos profile image
Bryant Morrill

Here is another great resource about Dry Transactions: youtube.com/watch?v=kkLaYoKOa-o

Collapse
 
thadeu profile image
Thadeu Esteves Jr

Another library to encapsulated this, is github.com/serradura/u-case.

Simple and beautiful method to avoid write many code.