Service objects allow Models, Routes, etc to more closely follow the single responsibility principle. The responsibility of a Model is to represent an object’s state, but often the responsibility to know how to i.e. interact with external services leaks into a Model.
A Service is a class that can encapsulate behavior. They are especially useful for capturing business logic, coordinating the interaction of two collaborating objects, and isolating side-effects.
Here are some guidelines to implementing services.
This is to mirror the signature of Proc
and represent that the purpose of the class is to encapsulate a single behavior.
module Services
module Queries
class GetUsers < Service
...
def self.call
...
end
end
end
end
A user of the service should know what behavior to expect from the name of the service.
module Services
module Commands
class UpdateUser < Service
...
def self.call
user.update(...) # side-effect
end
end
end
end
module Services
module Queries
class FindUser
...
def call
user.where(...) # side-effect-free
end
end
end
end
Query services should be tested via their return value, Command services should be tested via their side-effects.
let (:luke) { User.create(id: 1, name: "Luke Skywalker") }
expect(Services::Queries::FindUser.call(1)).to eq({name: "Luke Skywalker"})
```ruby
Services::Commands::UpdateUser.call(luke, "Darth Vader")
expect(luke).to have_received(:update).with(name: "Darth Vader")
FormatMarketoLeadFields
EnrichPerson
DeliverWebhook
MarketoLeadFields
PersonEnricher
Webhook
WebhookDeliverer
The Service can optionally inherit from a generic Service object with a single public instance method call
that instantiates & invokes services
class Services::BaseService
def self.call(*args)
new(*args).call
end
end
class Services::Command::UpdateLead
def initialize(id, params)
@id = id
@params = params
end
def call()
...
end
end