Decorating Service Objects - FunctionalObject

A common pattern used in Ruby programs are Service Objects. They help enforce the Single-responsibility Principle throughout the app, which in turn makes the program easier to test, debug and reason about.

result = CheckoutService.new(item: item, user: user).call
  if result.successful?
    ...

At my workplace, we use service objects extensively in our codebase. One modification we made to them was to shorten the code it takes to invoke the service. All our service objects are invoked like this:

result = CheckoutService.(item: item, user: user)
  if result.successful?
    ...

It's a minor code shortening, but it also enforces a single interface to invoke the class within the entire program. It's like we are saying "Only invoke services with the #call method".

Implementation

Implementing this pattern is pretty straightforward.

First, we define a base class for all our Service Object's.

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

    def call
      raise "Not Implemented"
    end
  end

Here, we do two things.

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

One, we define a class method self#call. It doesn't care what arguments it receives (see this footnote if you're wondering what (...) does), it just forwards them to new() and then immediately invokes #call by chaining .() behind it.

def call
    raise "Not Implemented"
  end

Two, we enforce that all subclasses define #call- we raise an error if it isn't implemented.

Then we make all our service classes inherit from our base class.

class FooService < ApplicationService
  def initialize(name: )
    @name = name
  end

  def call
    "Hello #{@name}"
  end
end

That's it. All classes that inherit from ApplicationService needs to adhere to the #call class signature, and can be invoked with the shorter method.

FooService.(name: "Dan")
=> "Hello Dan"

One step further

As you can see, this pattern can be applied to any Ruby class. In the future, you might have other classes in your program that you also want to invoke with this shorthand method. In that case, you can implement it as a mixin.

module FunctionalObject
  module ClassMethods
    def call(...)
      new(...).()
    end
  end

  def self.included(klass)
    klass.extend(ClassMethods)
  end

  def call
    raise NotImplementedError
  end
end

Now any class that wants to behave similarly can just include the mixin, like so.

class ApplicationService
  includes FunctionalObject
end

Footnotes

Method Forwarding Operator

Since Ruby 2.7, the (...) operator can be used to forward all arguments from a forwarding method to a concrete method.

def forwarding_method(...)
    concrete_method(...)
  end

See the official docs for more info.

23