Decorating Service Objects - FunctionalObject

gohdaniel15

Daniel Goh

Posted on July 30, 2021

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?
    ...
Enter fullscreen mode Exit fullscreen mode

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?
    ...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Here, we do two things.

  def self.call(...)
    new(...).()
  end
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

class ApplicationService
  includes FunctionalObject
end
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

See the official docs for more info.

💖 💪 🙅 🚩
gohdaniel15
Daniel Goh

Posted on July 30, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related