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
endA 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
endmodule Services
module Queries
class FindUser
...
def call
user.where(...) # side-effect-free
end
end
end
endQuery 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
DeliverWebhookMarketoLeadFields
PersonEnricher
Webhook
WebhookDelivererThe 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