
Manufacturable
Manufacturable is a factory that builds self-registering objects.
It leverages self-registration to move factory setup from case statements, hashes, and configuration files to a simple DSL within the instantiable classes themselves. Giving classes the responsibility of registering themselves with the factory does two things. It allows the factory to be extended without modification. And, it leaves the factory with only one responsibility: building objects.
Motivation
We wrote Manufacturable, so we wouldn't have to keep modifying our factory code every time we needed to add functionality to our applications. For example, consider this factory:
class AutomobileFactory
def self.build(type, *args)
case type
when :sedan
Sedan.new(*args)
when :coupe
Coupe.new(*args)
when :convertible
Convertible.new(*args)
end
end
end
If you want to start building Hatchback objects, you'll need to modify the
factory. To solve this problem in Ruby, factories are often built using
metaprogramming, like this:
class AutomobileFactory
def self.build(type, *args)
Object.const_get(type.capitalize)&.new(*args)
end
end
But, this very simple factory relies on a convention: the type symbol must
match the name of the class. This means that classes with namespaces, or
symbols with underscores will not work. In other words, you could not use the
symbol :four_door to build a Sedan object.
Manufacturable solves these problems by allowing classes to register themselves with the factory using a key of their choosing. This means you never have to modify the factory code again.
Usage
The Basics
A class may register itself with Manufacturable like this:
class Sedan
extend Manufacturable::Item
corresponds_to :four_door
end
Extending Manufacturable::Item adds the Manufacturable DSL to the class.
Calling corresponds_to with a key registers that class with the factory.
Once registered, a class may be instantiated like this:
Manufacturable.build(Object, :four_door, *args)
Note the first parameter. This is the parent class of the registered class.
In this case, the parent class happens to be Object. So, Manufacturable
registered the Sedan class under the Object namespace to prevent key
collision. To instantiate the Sedan, we need to request the :four_door
key from the Object namespace.
For convenience, Manufacturable provides an ObjectFactory to build objects
that are stored in the Object namespace:
Manufacturable::ObjectFactory.build(:four_door, *args)
In most cases, though, your class will actually inherit from a specific class
other than Object. For example, it is likely that the Sedan class would
inherit from an Automobile class. If that were the case, you would pass
Automobile as the first parameter to Manufacturable.build:
class Automobile
extend Manufacturable::Item
end
class Sedan < Automobile
corresponds_to :four_door
end
Manufacturable.build(Automobile, :four_door, *args)
That's all you need to know to begin using Manufacturable. But, it's not all there is to know. Manunfacturable allows you to:
- Configure your own factory classes
- Define a default manufacturable item
- Register multiple classes under the same key within a namespace
- Register a class to correspond with an entire namespace
Using Factory Classes
Manufacturable also has a DSL for creating factories:
class AutomobileFactory
extend Manufacturable::Factory
manufactures Automobile
end
Extending Manufacturable::Factory adds the DSL to the factory class.
Calling manufactures with a class designates it as the namespace for the
factory.
Once configured, you can use the AutomobileFactory to build objects from
classes in the Automobile namespace:
AutomobileFactory.build(:four_door, *args)
Defining a Default Manufacturable Item
What happens when Manufacturable is unable to find the key you're looking for?
That depends on what you tell Manufacturable. By default, it will return nil
when it does not find a class registered at a specific key. But, you can also
configure Manufacturable's response. This allows you to implement the null
object pattern.
class NullAutomobile < Automobile
default_manufacturable
end
Now, your calling code does not have to check for nil before calling a method
on the class:
AutomobileFactory.build(:lemon, *args).drive
Registering Multiple Classes Under the Same Key within a Namespace
Manufacturable allows you to register multiple classes under the same key:
class StandardEngine < Component
corresponds_to :sedan
end
class AutomaticTransmission < Component
corresponds_to :sedan
end
class PowerfulEngine < Component
corresponds_to :coupe
end
class ManualTransmission < Component
corresponds_to :coupe
end
Then, when you request that key, you'll receive an array containing a new instance of each class registered under that key.
ComponentFactory.build(:sedan, *args)
# => [#<StandardEngine:0x00007fad6c07e858>, #<AutomaticTransmission:0x00007fad6c07e808>]
ComponentFactory.build(:coupe, *args)
# => [#<PowerfulEngine:0x00007fad6c07e858>, #<ManualTransmission:0x00007fad6c07e808>]
Registering a Class to Correspond with an Entire Namespace
Manufacturable will also let you register a class that corresponds with all of the keys in a namespace:
class HeadLight < Component
corresponds_to_all
end
Now, the ComponentFactory will include HeadLight objects for both the
:sedan and :coupe.
ComponentFactory.build(:sedan, *args)
# => [
# #<StandardEngine:0x00007fad6c07e858>,
# #<AutomaticTransmission:0x00007fad6c07e808>,
# #<HeadLight:0x00007fad6c07e667>
# ]
ComponentFactory.build(:coupe, *args)
# => [
# #<PowerfulEngine:0x00007fad6c07e858>,
# #<ManualTransmission:0x00007fad6c07e808>,
# #<HeadLight:0x00007fad6c07e667>
# ]
Installation
Add this line to your application's Gemfile:
gem 'manufacturable'
And then execute:
$ bundle install
Or install it yourself as:
$ gem install manufacturable
If you are using Manufacturable with Rails, you'll need an initializer to tell manufacturable where the classes are, so they can be autoloaded.
Manufacturable.config do |config|
config.paths << Rails.root.join('app', 'automobiles')
config.paths << Rails.root.join('app', 'components')
end
Development
After checking out the repo, run bin/setup to install dependencies. Then,
run rake spec to run the tests. You can also run bin/console for an
interactive prompt that will allow you to experiment. To install this gem
onto your local machine, run bundle exec rake install.
Contributing
Bug reports and pull requests are welcome on GitHub.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the Manufacturable project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.
Acknowledgements
Manufacturable was inspired by work we did at Entelo on Industrialist. We will be forever grateful to the people at Entelo for giving us the opportunity to work on things we're still proud of today.