Here's a video version of this article if you prefer to watch instead:
And you can check out the code on Github if you want to try it out.
What the Abstract Factory pattern allows you to do is to isolate conditional instantiations of related objects so that your client code can be extended without changing it.
But to illustrate this, let's look at some code.
I've got a lot of code, but don't worry I'm gonna explain all of it.
So first thing first, I want to show you how we're going to use it. So let's look at the client code.
# abstract_factory.rb
# Product interface
class Chair
def leg_count = raise('not implemented')
def cushion? = raise('not implemented')
end
# Product interface
class Table
def material = raise('not implemented')
end
# Modern (product)
class ModernChair < Chair
def leg_count = 3
def cushion? = false
end
# Vintage (product)
class VintageChair < Chair
def leg_count = 4
def cushion? = true
end
# Modern (product)
class ModernTable < Table
def material = "glass"
end
# Vintage (product)
class VintageTable < Table
def material = "wood"
end
# Abstract Factory
# The abstract class defines the interface of the variant types
# Makes sure all subclases have the exact same behavior
class FurnitureFactory
# Returns an abstract Chair
def create_chair = raise('not implemented')
# Returns an abstract Table
def create_table = raise('not implemented')
end
# The variant type class decides the instance type
class ModernFurnitureFactory < FurnitureFactory
def create_chair = ModernChair.new
def create_table = ModernTable.new
end
# The variant type class decides the instance type
class VintageFurnitureFactory < FurnitureFactory
def create_chair = VintageChair.new
def create_table = VintageTable.new
end
def client_code(factory)
chair = factory.create_chair
table = factory.create_table
puts "Chair has #{chair.leg_count} legs and #{chair.cushion? ? '' : 'no '} cushion."
puts "Table is made of #{table.material}."
end
# In action!
modern_factory = ModernFurnitureFactory.new
vintage_factory = VintageFurnitureFactory.new
client_code(modern_factory)
puts "-" * 72
client_code(vintage_factory)
This is basically how you're going to call your abstract factory from somewhere, for example from an endpoint.
I'm modeling a furniture store website, where you might have a furniture category that you want to use as a products filter.
In this particular example, I've left out the part where you get the category from a user submitted form and you dynamically create a factory based on that.
Here, we're building the factories by hand.
So you can see I'm trying out both a modern category, by creating a modern furniture factory, and a vintage category by creating a vintage furniture factory.
Once I have the factory, I give it to the client code method and it will call create_chair
, and create_table
on that factory object.
The most important thing to note here is that the client_code
method can support an unlimited number of factories as long as they respond to the create_chair
, and create_table
methods.
This is important because this is how you can extend your code, without changing it. It's called the open/closed principle, which says classes should be open for extension but closed for modification.
So, this client_code
method can be extended by adding new categories of furniture without changing anything inside of it. Because the changes we need to make, are outside of this client_code
method.
Maybe this doesn't look like a big deal, but imagine if the client_code
method, or similar logic was used 20 times throughout your codebase.
Whenever you had to add a new category, you'd had to update all 20 places to use the new category.
So I hope you can see how applying the abstract factory pattern to this client_code
method can make it more extendable.
Now let's see how it works.
If we take a look at the ModernFurnitureFactory
class, and the VintageFurnitureFactory
class, we can see that they both implement the same methods, namely create_chair
, and create_table
.
# abstract_factory.rb
class ModernFurnitureFactory < FurnitureFactory
def create_chair = ModernChair.new
def create_table = ModernTable.new
end
class VintageFurnitureFactory < FurnitureFactory
def create_chair = VintageChair.new
def create_table = VintageTable.new
end
They have to. Because otherwise the client_code
method wouldn't work. And we can even enforce that all these factory classes implement those two methods by making them inherit from a base class that raises when those two methods are called.
So the subclass, i.e. the ModernFurnitureFactory
class, and the VintageFurnitureFactory
class have to overwrite both of those methods.
It's not required to have that base class, because you'll get an undefined method if you call methods that do not exist, but it's good to have it as documentation for other developers.
Another way you could achieve the same goal, is to use shared tests to make sure that all these factory classes respond to the same methods.
Ok, moving on...
We can now look at how those methods work.
In the case of ModernFurnitureFactory
, the create_chair
method returns a ModernChair
object, and the create_table
method returns a ModernTable
object.
# abstract_factory.rb
class ModernFurnitureFactory < FurnitureFactory
def create_chair = ModernChair.new
def create_table = ModernTable.new
end
Similarly, the create_chair
method for the VintageFurnitureFactory
class returns a VintageChair
object, and the create_table
method returns a VintageTable
object.
# abstract_factory.rb
class VintageFurnitureFactory < FurnitureFactory
def create_chair = VintageChair.new
def create_table = VintageTable.new
end
As you can see the type of object you'll get back is determined by the class of the factory, as opposed to the factory method pattern where the object you get back is determined by a method.
If you're not familiar with the factory method, check out my Factory Method pattern article.
Now if we look at the ModernChair
and ModerTable
classes, or the VintageChair
and VintageTable
classes, we can see that they too have to respect a contract, or an interface. In other words, they too need to respond to the same methods.
class ModernChair < Chair
def leg_count = 3
def cushion? = false
end
class VintageChair < Chair
def leg_count = 4
def cushion? = true
end
class ModernTable < Table
def material = "glass"
end
class VintageTable < Table
def material = "wood"
end
Otherwise the client code that uses them will raise an exception if it cannot call those methods on all the different types of objects.
And again, you have the option to use a base class, or parent class if you want to call it that, or you can enforce it via shared tests.
So if we look at this entire file, we can see that we basically have two different kinds of furniture, and we can programmatically use either one.
By determining the category, we can create a factory object based on that category, and that factory object can create products in that specific category.
All of this code lives in one file (namely the abstract_factory.rb
file) because it's easier for you to see the whole picture, but you can see how it could be arranged into multiple files to clean the whole thing up if you check out the lib folder.
In the lib
folder here, I have an Endpoint
class that is the top entry point.
# lib/endpoint.rb
$:.unshift(__dir__) unless $:.include?(__dir__)
require "furniture/factory"
class Endpoint
def self.category(params)
category = params["category"].to_sym
factory = if category == :modern
Furniture::Modern::Factory.new
elsif category == :vintage
Furniture::Vintage::Factory.new
else
Furniture::Regular::Factory.new
end
# factory = Furniture::Factory.for(category)
chair = factory.create_chair
table = factory.create_table
puts <<~TEXT
Chair has #{chair.leg_count} legs and #{chair.cushion? ? '' : 'no'} cushion.
Table is made of #{table.material}.
TEXT
end
end
params = { "category" => "xxx" }
Endpoint.category(params)
It's got a category
method that receives some params. The params refer to the request params.
Depending on what web framework you are using, you'll probably have a slightly different way of accessing the params. But it's going to be a hash like the one illustrated in the code snippet above.
And so you'll get access to that params hash in some form or another, at which point you can extract the category out of it, and create your factory object.
# lib/endpoint.rb
class Endpoint
def self.category(params)
category = params["category"].to_sym
factory = if category == :modern
Furniture::Modern::Factory.new
elsif category == :vintage
Furniture::Vintage::Factory.new
else
Furniture::Regular::Factory.new
end
# factory = Furniture::Factory.for(category)
chair = factory.create_chair
table = factory.create_table
puts <<~TEXT
Chair has #{chair.leg_count} legs and #{chair.cushion? ? '' : 'no'} cushion.
Table is made of #{table.material}.
TEXT
end
end
The simplest and less flexible option is to use an if/else
statement and create the factory like this. But if you know how the factory method pattern works, you can replace that conditional with a call to a method.
Like this.
# lib/endpoint.rb
class Endpoint
def self.category(params)
category = params["category"].to_sym
factory = Furniture::Factory.for(category)
chair = factory.create_chair
table = factory.create_table
puts <<~TEXT
Chair has #{chair.leg_count} legs and #{chair.cushion? ? '' : 'no'} cushion.
Table is made of #{table.material}.
TEXT
end
end
And here's how that method looks like. It takes the category and it tries to determine a class name based on it, and if it cannot find a class, it falls back to the RegularFactory
class.
# lib/factory.rb
require "furniture/vintage/factory"
require "furniture/modern/factory"
require "furniture/regular/factory"
module Furniture
class Factory
TYPES = {
vintage: Vintage::Factory,
modern: Modern::Factory
}
def self.for(type)
(TYPES[type] || Regular::Factory).new
end
end
end
Each category has its own folder now that holds a factory for the category, and its products.
There's also a products
folder which holds the interfaces (or the base classes) for the products.
The Base
class for all factories lives in this base
file.
So let's run this Endpoint.category
method to see what it does.
Chair has 1 legs and no cushion.
Table is made of wood.
I've set the category in the params hash to "vintage". And then I'm calling the category
method, and I'm passing in the params hash.
And as you can see, it prints "Chair has 1 legs and no cushion. Table is made of wood." because those are the properties defined in the VintageChair
object and the VintageTable
object.
But if we change the category to "modern", we'll get a different message back.
Chair has 3 legs and no cushion.
Table is made of glass.
And lastly if we put "regular" in the category, or if we put a category that doesn't exist, we'll get the message corresponding with the "regular" objects.
Chair has 4 legs and cushion.
Table is made of plastic.
So there you have it, that is the Abstract Factory pattern in Ruby.