Skip to content

Instantly share code, notes, and snippets.

@andypike
Last active October 21, 2022 12:25
Show Gist options
  • Save andypike/6fd735862302b09f5259 to your computer and use it in GitHub Desktop.
Save andypike/6fd735862302b09f5259 to your computer and use it in GitHub Desktop.
Self registering ruby classes
class Toolbox
@tools = {}
def self.register(tool)
@tools[tool.material] = tool
end
def self.tool_for(material)
@tools.fetch(material) { BareHands }
end
end
module WorksWithMaterials
def self.included(base)
base.send(:extend, ClassMethods)
end
module ClassMethods
attr_accessor :material
def works_with(material)
self.material = material
Toolbox.register(self)
end
end
end
class BareHands
def build_something
puts "I use my bare hands and work with anything else"
end
end
class Saw
include WorksWithMaterials
works_with :wood
def build_something
puts "I'm a saw and I can work with wood"
end
end
class Anvil
include WorksWithMaterials
works_with :metal
def build_something
puts "I'm an anvil and I can work with metal"
end
end
tool = Toolbox.tool_for(:wood)
tool.new.build_something
# => "I'm a saw and I can work with wood"
tool = Toolbox.tool_for(:metal)
tool.new.build_something
# => "I'm an anvil and I can work with metal"
tool = Toolbox.tool_for(:fabric)
tool.new.build_something
# => "I use my bare hands and work with anything else"
@andypike
Copy link
Author

The idea here is to make it easy to extend a system without modifying existing code (open-closed principle). In this example we have a ToolBox that will hold our tools. Then, when we want to work with a particular material, we ask the ToolBox for the tool that works with that material.

The tool classes register themselves with the ToolBox when they are defined and specify which material they support. The ToolBox now has an internal collection of tools (classes). When we want a tool for a specific material we just ask for it and the ToolBox returns it.

In the case where there is no supported tool for the material, we use the Null Object Pattern to return a default object. In this example, the BareHands class. Depending on your usage, this can do nothing when called or it could do some default behaviour.

This allows us to switch implementation and have a default to fallback to without using a single conditional or some meta-programming magic (like constructing a class name and using const_get for example). It also means that if we want to support more tools in the future, we just need to add a new tool class and no other existing code needs to change in our system.

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