Skip to content

entelo/industrialist

Repository files navigation

Gem Version Maintainability Test Coverage CircleCI

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.