Suppose I have an inside Runner class that needs access to an outside Shell service
class Runner
def run(...)
...
stdout,_ = shell.assert_exec("docker run #{args} #{image_name} sh")
...
end
end
The Runner object locates the shell object using the nearest_ancestors mix-in:
require_relative 'nearest_ancestors'
class Runner
def initialize(parent, ...)
@parent = parent
...
end
attr_reader :parent
private
include NearestAncestors
def shell
nearest_ancestors(:shell)
end
end
module NearestAncestors
def nearest_ancestors(symbol)
who = self
loop {
unless who.respond_to? :parent
fail "#{who.class.name} does not have a parent"
end
who = who.parent
if who.respond_to? symbol
return who.send(symbol)
end
}
end
end
All objects know their parent object and nearest_ancestors chains back parent to parent to parent until it finds an object with the required symbol or runs out of parents. I can create a root object that simply holds the external-adaptors (eg shell). Conceptually, this root object lives at the boundary between the outside and the inside.
require 'sinatra/base'
require_relative 'shell'
require_relative 'runner'
class MicroService < Sinatra::Base
post '/run' do
runner.run(...)
end
def shell
@shell ||= Shell.new(self)
end
def runner
@runner ||= Runner.new(self, ...)
end
end
All the internal objects can access all the external-adaptors. I love how trivial moving a piece of code from one class to another class becomes. Another thing I love about this pattern is the effect it has on my tests.
require_relative 'shell_stubber'
...
class RunnerTest < MiniTest::Test
def test_runner_run_with_stubbed_shell
@shell = ShellStubber.new(self)
...
runner.run(...)
...
end
attr_reader :shell
def runner
@runner ||= Runner.new(self, ...)
end
end
- In the MicroService class self refers to the MicroService object which becomes the parent used in nearest_ancestors. Thus in runner.run, shell resolves to shell inside the MicroService object.
- In the RunnerTest class self refers to the RunnerTest object which becomes the parent used in nearest_ancestors. Thus in runner.run, shell resolves to shell inside the RunnerTest object.
No comments:
Post a Comment