Toritori
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.