Industrialist
Industrialist makes your factory code easy to extend and resilient to change.
It was inspired by the Gang-of-Four factory method and abstract factory patterns.
Factory code often involves a case statement. If you are switching on a key in order to choose which class to build, you have a factory:
def automobile(automobile_type)
case automobile_type
when :sedan
Sedan.new
when :coupe
Coupe.new
when :convertible
Cabriolet.new
end
end
This code often lives inside a class with other responsibilities. By applying the Single Responsibility Principle, you can extract it into a factory class:
class AutomobileFactory
def self.build(automobile_type)
automobile_klass(automobile_type)&.new
end
def self.automobile_klass(automobile_type)
case automobile_type
when :sedan
Sedan
when :coupe
Coupe
when :convertible
Cabriolet
end
end
end
AutomobileFactory.build(:sedan) # => #<Sedan:0x00007ff64d88ce58>
Another way to do this is with a hash:
class AutomobileFactory
AUTOMOBILE_KLASSES = {
sedan: Sedan,
coupe: Coupe,
convertible: Cabriolet
}.freeze
def self.build(automobile_type)
AUTOMOBILE_KLASSES[automobile_type]&.new
end
end
AutomobileFactory.build(:coupe) # => #<Coupe:0x00007ff64b6a372>
But, both of these approaches require you to maintain your factory by hand. In order to extend these factories, you must modify them, which violates the Open/Closed Principle.
The Ruby way to do this is with conventions and metaprogramming:
class AutomobileFactory
def self.build(automobile_type, *args)
Object.get_const("#{automobile_type.capitalize}").new(*args)
end
end
But, factories of this type also have issues. If your convention changes, you'll have to edit the factory, which violates teh Open/Closed principle. Or, if your keys are not easily mapped to a convention, you won't be able to use this type of factory. For example, the Cabriolet
class above corresponds to the key :convertible
.
You can find a deeper dive into the motivations behind Industrialst here.
Usage
Industrialist manages a factory of factories, so you don't have to. Setting it up is easy. When you create a factory class, extend Industrialist::Factory
, and tell Industrialist the base class of the classes your factory will manufacture:
class AutomobileFactory
extend Industrialist::Factory
manufactures Automobile
end
Next, tell Industrialist that the base class is manufacturable by extending Industrialist::Manufacturable
:
class Automobile
extend Industrialist::Manufacturable
end
And, finally, tell each of your subclasses what key to register themselves under:
class Sedan < Automobile
corresponds_to :sedan
end
class Coupe < Automobile
corresponds_to :coupe
end
As the subclasses are loaded by Ruby, they register themselves with the appropriate factory so that you can do this:
AutomobileFactory.build(:sedan) # => #<Sedan:0x00007ff64d88ce58>
Manufacturable classes may also correspond to multiple keys:
class Cabriolet < Automobile
corresponds_to :cabriolet
corresponds_to :convertible
end
By default, Industrialist factories will return nil
when built with an unregistered key. If you would instead prefer a default object, you can designate a manufacturable_default
.
class PlaneFactory
extend Industrialist::Factory
manufactures Plane
end
class Plane
extend Industrialist::Manufacturable
end
class Biplane < Plane
manufacturable_default
corresponds_to :biplane
end
class FighterJet < Plane
corresponds_to :fighter
end
PlaneFactory.build(:bomber) # => #<Biplane:0x00007ffcd4165610>
Industrialist can accept any Ruby object as a key, which is handy when you need to define more complex keys. For example, you could use a hash:
class TrainFactory
extend Industrialist::Factory
manufactures Train
end
class Train
extend Industrialist::Manufacturable
end
class SteamEngine < Train
corresponds_to engine: :steam
end
class Diesel < Train
corresponds_to engine: :diesel
end
class Boxcar < Train
corresponds_to cargo: :boxcar
end
class Carriage < Train
corresponds_to passenger: :carriage
end
class Sleeper < Train
corresponds_to passenger: :sleeper
end
TrainFactory.build(engine: :diesel) # => #<Diesel:0x00007ff64f846640>
For convenience, you can choose not to define your own factories. Instead, just use Industrialist directly:
Industrialist.build(:plane, :bomber) # => #<Biplane:0x00007ffcd4165610>
Industrialist.build(:train, engine: :diesel) # => #<Diesel:0x00007ff64f846640>
Industrialist.build(:autombile, :sedan) # => #<Sedan:0x00007ff64d88ce58>
Installation
Add this line to your application's Gemfile:
gem 'industrialist'
And then execute:
$ bundle
Or install it yourself as:
$ gem install industrialist
If you are using Industrialist with Rails, you'll need to
Industrialist.config do |config|
config.manufacturable_paths << Rails.root.join('app', 'planes')
config.manufacturable_paths << Rails.root.join('app', 'trains')
config.manufacturable_paths << Rails.root.join('app', 'automobiles')
end
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/entelo/industrialist.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the Industrialist project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.