Ruby Service Objects without gems
Tags: ruby, rails, testing
Published: 2019-08-05 19:49:19

Sometimes we use gems like [Trailblazer](https://github.com/trailblazer/trailblazer) or [Interactor](https://github.com/collectiveidea/interactor) just to separate business logic from controllers and models, and sometimes, a couple of PORO's and a simple convention is enough:

Here is a general outline of my favourite set of conventions for services object:

- inherit from a base service that holds shared behaviour
- have a single class-level _command method_ `run` accepting keyword arguments, that *always* returns an instance of the service class
- have an instance level `status` _query method_ to check the success of the action (usually `:ok` and`:error`)
- have an instance level `result` _query method_ that holds any external object needed as an outcome

> I am using _query_ and _command_ in the sense Sandi Metz does:
> - _query_ is a method that gets information but does not change the state of the receiver, whereas
> - _command_ may or may not return something but always changes the state of the receiver

I always use `run` and have the name of the class describe the action, other people, like the very wise Xavier Noria, prefer to make service's main method a meaningful verb, but I find it just another thing to remember.

Let's see it in action with an example, I will use authentication, not because you should program your own (you shouldn't), but because it's a common business logic which shall make the fir of the ServiceObject better:

A base service to inherit from:
```ruby
class Service
  attr_reader :result, :status

  def self.run(**args)
    new(**args).tap(&:run)
  end

  def initialize(*)
    raise NotImplemented, "must be defined by subclasses"
  end
end
```

A sample service with a familiar logic:
```ruby
module Services
  class UserAuthenticate < ::Service
    def initialize(username:, password:, user_model: User)
      @username = username
      @password = pasword
    end

    def run
      if user&.authenticate(@password)
        @result = user
        @status = :ok
      else
        @result = nil
        @status = :error
      end
    end

    private

    def user
      user_model.find_by(username: @username)
    end
  end
end
```

And and example of usage in an quite unoriginal rails app:
```ruby
class SessionsController < ApplicationController
  def create
    if user_authentication.status == :ok
      session[:user_id] = user_authentication.result.id
      redirect_to root_path
    else
      render :new, flash: { error: t('.wrong_email_or_password') }
    end
  end

  def destroy
    session[:user_id] = nil
    redirect_to root_path
  end

  private

  def user_authentication  
    @user_authentication ||= UserAuthentication.run(session_params)
  end

  def session_params
    params.require(:user).permit(:username, :password)
  end
end
```

What, ah, you were intrigued by the `user_model: User` part?

Ok lets see how to test this (in minispec):

```ruby
describe UserAuthentication do
  let(:user_class) { Object.new }
  let(:user) { Minitest::Mock.new }
  let(:good_pass) { 'good_pass' }
  subject do
    user_class.stub(:find_by, user) do
      UserAuthenticate.run('username', user_class: user_class)
    end
  end

  describe "valid password" do
    before do
      user.expect(:authenticate, true, [good_pass])
    end

    it "returns status ok" do
      value(subject.status).must_equal(:ok)
    end

    it "has a result of user object" do
      value(subject.result).must_equal(user)
    end

    it "calls authenticate on the user model" do
      subject
      user.verify
    end
  end

  describe "invalid password" do
    # I am sure you can deduce this part :-)
  end
end

```