Skip to content

Instantly share code, notes, and snippets.

@JoshCheek
Last active March 12, 2021 21:16
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JoshCheek/1cc7f13a676a0d4e9930f8a13a388816 to your computer and use it in GitHub Desktop.
Save JoshCheek/1cc7f13a676a0d4e9930f8a13a388816 to your computer and use it in GitHub Desktop.
A myriad of ways to implement "service objects"
# Note1: all `...` below mean "etcetera", like psuedocode, not implying the new Ruby 3 syntax
# Note2: `obj.()` is syntactic sugar for `obj.call()`
# Given an invocation like this:
OnboardOrganization.(arg1, arg2, ...)
# Here are some possible ways you could implement it:
# 1. If the definition fits in a s single method, you can toss it on a module
# You'd choose a module b/c you don't want a class here since you don't want to
# instantiate it. However you probably don't want to include/extend it either,
# given you named the method `call`, so it's not a perfect match.
module OnboardOrganization
def self.call(arg1, arg2, ...)
# ...
end
end
# 2. Approximately equivalent to #1, but avoids the module
# Maybe you don't want to imply the thing should be included / extended, so you
# don't want to use a module like in #1, so you make it a normal object. Kind of
# a better choice, except it doesn't have a nice inspection (unless you define
# `inspect` on it, too, like the main object), and it can't depend on any constants.
OnboardOrganization = Object.new
def OnboardOrganization.call
# ...
end
# 3. Equivalent to #2, but maybe a little bit better of a fit, since this is what
# Procs are for. Also brings back constants (ie something you init once outside
# the function, and then refer back to), exept you would save them in local
# variables instead of constants, but they would function identically.
OnboardOrganization = lambda do |arg1, arg2, ...|
# ...
end
# 4. Equivalent to #1, but adds helper functions
# If the implementation is complicated enough that you want helper functions,
# you can change the initial module version like this:
module OnboardOrganization
extend self
def call(arg1, arg2, ...)
helper1(arg1) + helper2(arg2) + ...
end
private
def helper1(arg)
# ...
end
# ...
end
# 5. Equivalent to #4, but facilitates include/extend
# This lets you do `OnboardOrganization.call`, like we initially wanted, or
# you can include it and then do `onboard_organization(arg1, arg2, ...)`
# note that you have to be careful with your helper method names b/c they'll get
# mixed into some other namespace.
module OnboardOrganization
extend self
def self.call(*args, &b)
onboard_organization *args, &b
end
private
# you could potentially make this public, but private is more likely to be correct
def onboard_organization(arg1, arg2, ...)
# ...
end
end
# 5. From #4, if you found yourself passing the same args to every function,
# then that would imply those args should be instance variables instead of local variables.
# So then you might change it to a class so that the common args to every method can be implicit:
class OnboardOrganization
def self.call(*args, &block)
new(*args, &block).call
end
def initialize(arg1, arg2, ...)
@arg1, @arg2 = arg1, arg2 # ...
end
def call
helper @arg2
end
private
def helper(arg2)
@arg1 + arg2 # ...
end
end
@jasonpilz
Copy link

Nice 👏

Another version of #1:

module OnboardOrganization
  module_function

  def call(arg1, arg2, ...)
    # ...
  end
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment