The Abstract Factory Pattern in Ruby

Feb 10, 2022 - 9 min read
The Abstract Factory Pattern in Ruby
Share

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.

SERIOUS ABOUT LEARNING RAILS?

Every other week you'll get at least 1 actionable tip on how to become a better Ruby on Rails developer.
    We won't send you spam. Unsubscribe at any time.
    Built with ConvertKit
    Cezar Halmagean
    Software development consultant with over a decade of experience in helping growing companies scale large Ruby on Rails applications. Has written about the process of building Ruby on Rails applications in RubyWeekly, SemaphoreCI, and Foundr.