Toritori

Maintainability Test Coverage Gem Version

Simple tool to work with Abstract Factories. It provides the DSL for defining a set factories and produce objects.

Installation

Add this line to your application's Gemfile:

gem 'toritori'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install toritori

Basic usage

First, add module to target class and define factory method

require 'toritori'

class MyAbstractFactory
  include Toritori

  factory :chair
end

You'll get a few methods:

# top level method similar to FactoryBot
MyAbstractFactory.create(:chair)

# a way for inspecting definitions of factory methods
MyAbstractFactory.factories         # => { chair: #<Toritori::Factory @name: :chair> }
MyAbstractFactory.factories[:chair] # => chair: #<Toritori::Factory @name: :chair>
# an alias methods that reads from factories hash
factory = MyAbstractFactory.chair_factory     # => chair: #<Toritori::Factory @name: :chair>

# Specific factory actually creates new objects
# top level method just calls it
MyAbstractFactory.factories[:chair].create

factory.base_class      #=> #<Class>
factory.creation_method #=> :new

This example above shows a rare case when we just create instances of anonymous class.

Setup options

In most cases we want to specify a class of objects we are aiming to produce. Or define a few methods ourself. Assume we have some class

class Table < Struct(:width, :height, :depth)
  # ...omitted...
end

To specify that we want to produce instances of Table class

class MyAbstractFactory
  factory :table, produces: Table
end

factory = MyAbstractFactory.table_factory
factory.base_class      #=> #<Table>
factory.creation_method #=> :new

MyAbstractFactory.create(:table, 80, 80, 120) #=> #<Table @width=80 @height=80 @depth=180>

By default, we just call new method to instantiate an object. Some times it's not possible

class MyAbstractFactory
  factory :file, produces: File, creation_method: :open
end

factory = MyAbstractFactory.file_factory
factory.base_class      #=> #<File>
factory.creation_method #=> :open

MyAbstractFactory.file_factory.create('/dev/null') # => #<File @path='/dev/null'>

Or you need to use another factory method

class MyAbstractFactory
  factory :user, produces: User do |**kw|
    FactoryBot.create(:user, **kw)
  end
end

factory = MyAbstractFactory.user_factory
factory.base_class      #=> #<User>
factory.creation_method #=> #<Proc>

Sub-classes

But the main feature of the library is a possibility to change or extend the produced object. It is achieved by defining a sub-class of the target class. That is why produces option is required in most cases

class MyAbstractFactory
  table_factory.subclass do
    attr_reader :shape

    def initialize(width, height, depth, shape)
      super(width, height, depth)
      @shape = shape
    end
  end
end

MyAbstractFactory.create(:table, 80, 80, 80, :round)
#=> #<Table @width=80 @height=80 @depth=180 @shape=:round>

However this operation alters a definition of factory

MyAbstractFactory.table_factory.base_class            #=> #<Class>
MyAbstractFactory.table_factory.base_class.superclass #=> #<Table>
MyAbstractFactory.table_factory.creation_method       #=> :new

Sometimes when sub-class definition is big it is better to put it into a separate file.

class ModernTable < Table
  # ... omitted ...
end

# Alternatively more generic code
class ModernTable < MyAbstractFactory.table_factory.base_class; end

class MyAbstractFactory
  table_factory.subclass produces: ModernTable
end

MyAbstractFactory.table_factory.base_class            #=> #<ModernTable>
MyAbstractFactory.table_factory.base_class.superclass #=> #<Table>
MyAbstractFactory.table_factory.creation_method       #=> :new

Note, that you should provide a child class otherwise you'll get exception Toritori::SubclassError It is possible to change creation method of a sub-class

class MyAbstractFactory
  table_factory.subclass creation_method: :create do
    def self.create(...)
      new(...)
    end
  end
end

MyAbstractFactory.table_factory.base_class            #=> #<Class>
MyAbstractFactory.table_factory.base_class.superclass #=> #<Table>
MyAbstractFactory.table_factory.creation_method       #=> :create

Following calls of subclass method will create new sub-class based on sub-class generated by previous invocation.

Inheritance

Let's imagine you need to the following setup

class MyAbstractFactory
  factory :chair
  factory :table
end

class ModernFactory < MyAbstractFactory
  chair.subclass do
    include ModernStyle
  end
end
class VictorianFactory < MyAbstractFactory
  chair.subclass do
    include VictorianStyle
  end
end

During inheritance child classes should get the same factories as parent class. Definition of factories is copied from parent to child class

copy = MyAbstractFactory.chair_factory.copy
copy.base_class == MyAbstractFactory.chair_factory.base_class
copy.creation_method #=> :new

It means that after the inheritance factories of child and parent are disconnected. Changes in factory definition in parent factory won't affect child classes and vice versa.

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. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/andriy-baran/toritori. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the Toritori project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.