Service objects are those concerned with a specific use-case in a system. A common example I’ve written time and again is Registration. Registration handles things like creating a User and its related objects, sending welcome emails, kicking off background jobs, etc. My applications are filled with them, and I love them all. They take complexity and isolate it so that it can be ignored when thinking about other objects, considered a “black box” when it’s working, and stubbed out in tests.
One way to make these even easier to stub out is to make their main entry point a method named call
. Why this name? It’s the name of the method used to invoke a Proc
in Ruby. This means that a service object can be stubbed in collaborators’ tests with a simple proc
, lambda
, or -> {}
call. It’s especially nice combined with dependency injection:
class Collaborator
def initialize(service, other_args)
@service = service
# ...
end
def work
# I can do that.
@service.call(self)
end
end
Collaborator.new(-> (x) {}, "other").work
As added bonuses, you can send the .call
message with the shortcut .()
, which is syntax sugar added to Ruby for this method specifically. It takes arguments: service.(arg1, arg2)
.
On your service object, you can also define call
on the class and delegate to a new instance to make the service object even easier to use:
class ServiceObject
def self.call(x, y, z)
new(x, y, z).()
end
def initialize(x, y, z)
@x, @y, @z = x, y, z
end
def call
# Work, work.
end
end
ServiceObject.(1, 2, 3)