Last active
August 17, 2022 16:12
-
-
Save adsteel/c1ddf54af59bba980fd5aa13d270b6d7 to your computer and use it in GitHub Desktop.
Search for the Perfect Service Object
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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