Factory

Manufacturable

Gem Version Build Status Maintainability Test Coverage

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:

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.