Skip to content

Instantly share code, notes, and snippets.

@adsteel
Last active August 17, 2022 16:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save adsteel/c1ddf54af59bba980fd5aa13d270b6d7 to your computer and use it in GitHub Desktop.
Save adsteel/c1ddf54af59bba980fd5aa13d270b6d7 to your computer and use it in GitHub Desktop.
Search for the Perfect Service Object
# PROBLEM
# Service objects are almost always just functions defined as classes. They should not be initialized, as they are not
# used to manage internal state. They accept parameters and return values. In Ruby, this creates a lot of boilerplate.
# How can we trim that down? Here are some explorations.
require 'pry'
require 'securerandom'
# A simple factory for uniform service object definition
def StructServiceBase(*args, keyword_init: false)
Struct.new(*args, keyword_init: keyword_init) do
def self.call(...); new(...).call; end
private_class_method :new
end
end
class StructService < StructServiceBase(:a, :b, :c, keyword_init: true)
def call
p [a, b, c]
end
end
StructService.call(a: 2, b: 3, c: 4)
# An eval'ed factory for uniform service object definition
# that supports default values
class ServiceSubclassUnavailable < StandardError; end
def EvalServiceBase(function_signature, subclass_name: nil)
args = function_signature.to_s.split(",").map { |arg| arg.split(/=|:/)[0] }.map(&:strip)
raise ServiceSubclassUnavailable, "class #{subclass_name} already exists" if subclass_name && Object.const_defined?(subclass_name)
service_class = subclass_name || "Service_#{SecureRandom.hex(5)}"
return Service(function_signature) if Object.const_defined?(service_class)
str = <<~STR
class #{service_class}
def initialize(#{function_signature})
#{args.map do |arg|
"self.#{arg}=#{arg}"
end.join("\n")}
end
private_class_method :new
attr_accessor #{args.map { |arg| ":#{arg}" }.join(", ")}
def self.call(...); new(...).call; end
end
STR
eval(str)
Object.const_get(service_class)
end
class EvalService < EvalServiceBase('a, b=2, c:, d: 4')
def call
p [a, b, c, d]
end
end
# this errors out due to duplicate class definition
# class OtherService < EvalServiceBase('a', subclass_name: :EvalService)
# end
EvalService.call(1, 2, c: 3)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment