FormService: a PORO ServiceObjects with a state
Tags: rails, ruby, patterns, oop
Published: 2023-01-30 15:44:40

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:

```ruby
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
```

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

```ruby
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
```

## 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](https://dev.to/povilasjurcys/using-formobject-pattern-in-ruby-apis-3f8c) 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:

```ruby
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
```

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:

```ruby
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
```

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:
```ruby
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
```

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:

```ruby
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
```

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

```ruby
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
```

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.