Improving callable service objects with private constructors in Ruby
Tags: ruby, rails, productivity
Published: 2020-11-25 10:06:41

We recently found a nice practical use case for private class methods in Ruby: the constructor methods of what we will refer to here as _"callable services"_ in [Ruby on Rails](https://rubyonrails.org/) projects.

## Callable Services ☎️

First off: what do we mean with "_callable services"_ in Ruby on Rails applications?

### Drawers 🗄

In the real world, hardly any Ruby on Rails web application only consists of simple, atomic [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) (create, read, update, delete) operations on plain resources. Even the famous [blog engine built in 15 minutes](https://www.youtube.com/watch?v=Gzj723LkRJY&feature=youtu.be) may at some point incorporate logic beyond the complexity of creating or deleting posts. Take for instance translations, previews, comment moderation, etc. Real world applications model a specific domain and therefore include control flows, procedures and _"business logic"_ tied to that domain. These bits are in fact what makes an app unique and interesting.

When it comes to organizing code, Rails people traditionally aim for [_"skinny controllers and fat models"_](http://weblog.jamisbuck.org/2006/10/18/skinny-controller-fat-model). But as models packed with responsibilities beyond modeling the domain soon tend to get overweight and hard to maintain, the community started to reach for [additional concepts and patterns to organize their domain specific code](https://codeclimate.com/blog/7-ways-to-decompose-fat-activerecord-models). One _"drawer"_ one may come across in a lot of Rails applications are _"services"_, also referred to as [_"service objects"_](https://avdi.codes/service-objects/) or _"procedures"_. They are usually organized under the `app/services/` directory and encapsulate functionality to handle domain-specific logic, such as checking our a cart, registering for the site or starting a subscription. [¹](#references)

Services are usually implemented as POROs ("plain old Ruby objects") which, upon a given input, perform a set of operations and return a predictable response. They are easy to unit test and help developers to maintain a sense for the bigger picture by hiding away the internal details in what appears to the outside as a large black-box-function. That is why people started to implement them as classes exposing only a single method, `call` or `run`. Depending on its internals and their complexity a service may create a new instance for each call or just utilize a single class method for its work:

```ruby
class Authenticator
  def self.call(user)
    # complex logic
  end
end

# invoking the service:
Authenticator.call(user)
```

The same service implemented to use a new instance for each call:

```ruby
class Authenticator
  def initialize(user)
    @user = user
  end

  def call
    # complex logic, operate on @user
  end
end

# invoking the service:
Authenticator.new(user).call
```

### Conventions 🧘

At bitcrowd we - as you know - [love conventions](https://github.com/bitcrowd/tickety-tick) and therefore usually strive for a common API for our classes in `app/services` in Rails projects. So even if we don't know what's in the box, we at least know we're dealing with a box… Picking up the previous example, this could look like this:

```ruby
class Authenticator
  def initialize(user)
    @user == user
  end

  def call
    # complex logic
  end

  private

  attr_reader :user

  def private_helper_method
    # some bits of logic, can operate on user
  end
end
```

So within the project all services follow the same general structure:
- a constructor takes all the core input data the service needs to do its work
- a single exposed `call` method invokes the service to perform the actual work

### Simplifications 🧹

The functionality encapsulated within a service is seen as one operational unit on the outside. So we probably won't interfere between initializing the service and calling it. If the data needed to be manipulated between `new` and `call`, we should probably rather think about drawing the boundaries between our objects differently instead. But having things clearly encapsulated, we could also simplify the service' API to `Service.call(<input-data>)` and hide the implementation details of it using a new instance for each call inside of it. Since Ruby 2.7, we also make use of the 3 dots argument forwarding syntax [²](#references) - also referred to as [_"forward everything"_](https://bugs.ruby-lang.org/issues/16253) - for the `.call` class method:

```ruby
class Authenticator
  def initialize(user)
    @user = user
  end

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

  def call
    # complex logic
  end
end
```

This API makes our service more streamlined and predictable. The service is easier to read and less verbose on the outside, hiding implementation details inside the class itself. We're also less likely to accidentally sneak code between `new` and `call` when invoking the service. And while it's aesthetically pleasing on the eye, it also allows us to write less verbose expectations in our unit tests for code which interacts with the service:

```ruby
# Before
let(:service_instance) { instance_double(Authenticator) }

it 'calls the service' do
  expect(Authenticator).to receive(:new).with(user).and_return(service_instance)
  expect(service_instance).to receive(:call)
  # ...
end

# After
it 'calls the service'
  expect(Authenticator).to receive(:call).with(user)
  # ...
end
```

While we previously needed two expectations to ensure both, the service being initialized with the right data and then being invoked, we can now do both in one step.

## Communication 📢

Even with the new simpler API, our services can of course still be called in "the old" way, initializing and calling the service in two steps. The API for this approach is public and aside from examples in the code or documentation, we don't have anything at hand to ensure the service is used as intended. People may still happily do `Authenticator.new(user).call` and use instance doubles in their tests… The new API only gives a hint on how to use services in the project, it does not actually encourage or enforce one unified style.

### Private Constructors to the Rescue 🚒

Turns out we can make use of Ruby's [`private_class_method`](https://ruby-doc.org/core-2.7.1/Module.html#method-i-private_class_method) method on `Module` to hide the constructor and make our intentions on how to use the services more obvious:

> Makes existing class methods private. Often used to hide the default constructor new.

Adapting our example:

```ruby
class Authenticator
  def initialize(user)
    @user = user
  end

  private_class_method :new

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

  def call
    # actual logic
  end
end
```

With this we introduce a new problem though: the visual overhead of the additional boilerplate code makes the actual service implementation harder to read and understand. One way to circumvent this and allow the readers to stay focussed on the actual business logic, would be extracting the boilerplate into a [concern](https://api.rubyonrails.org/classes/ActiveSupport/Concern.html):

```ruby
module Callable
  extend ActiveSupport::Concern

  included do
    private_class_method :new
  end

  class_methods do
    def call(...)
      new(...).call
    end
  end
end
```

We can then shorten the service to:

```ruby
class Authenticator
  include Callable

  def initialize(user)
    @user = user
  end

  def call
    # actual logic
  end
end
```

This ensures our service is now used as intended. Trying to call its parts separately as `Authenticator.new(user).call` results in an error:

```
NoMethodError: private method `new' called for Authenticator:Class
```

Doing so, we can a clean and concise outer API for our services while not sacrificing readability on its internals. Invoking a service with its single exposed `call` class method ensure we pass the initial data to set up the state it needs to perform its work and then immediately trigger the actual _"work"_ part.

## TL;DR

Use `private_class_method` to hide the initializer of your service objects for a clearer API and less boilerplate in tests 💅.

## References

* 1: [https://blog.appsignal.com/2020/06/17/using-service-objects-in-ruby-on-rails.html](https://blog.appsignal.com/2020/06/17/using-service-objects-in-ruby-on-rails.html)
* 2: [https://github.com/ruby/ruby/blob/v2_7_0/NEWS#label-Other+miscellaneous+changes](https://github.com/ruby/ruby/blob/v2_7_0/NEWS#label-Other+miscellaneous+changes)