Skip to content

Instantly share code, notes, and snippets.

@bentona
Last active January 16, 2019 23:41
Show Gist options
  • Save bentona/ae93018f10b83be94d86b1ee909fb075 to your computer and use it in GitHub Desktop.
Save bentona/ae93018f10b83be94d86b1ee909fb075 to your computer and use it in GitHub Desktop.

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.

Services should have a single public class method named call

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

Services should be namespaced according to their purity

A user of the service should know what behavior to expect from the name of the service.

Command services have a side effect

module Services
  module Commands
    class UpdateUser < Service
      ...
      def self.call
        user.update(...) # side-effect
      end
    end
  end
end

Query services are pure

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")

The name of a service should be a verb describing exactly what the service does - and not a noun.

Good

FormatMarketoLeadFields
EnrichPerson
DeliverWebhook

Bad

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment