David Morales David Morales
/
A factory machine turning identical boxes into three different objects, with a ruby emblem.

When you think about object-oriented design, you think about which messages objects send each other and who knows what. But there’s a prior question that usually goes unnoticed: who creates the objects? Creating an object looks like a trivial detail, but every new is a coupling point: the code that writes it becomes tied to a concrete class, its name, and its arguments.

As long as only one class is possible, that coupling is harmless. The problem shows up when you need to choose between several classes at runtime. That knowledge, scattered across the codebase, is expensive to change, and that’s exactly why factory patterns exist: to give that decision a single home.

In this article we’ll look at the problem from scratch, all the way up to the Factory Method pattern.

The problem: a repeated decision

Imagine an application that imports contacts from files. Each format has its own importer, and all of them respond to the same message:

class CsvImporter
def import(path)
# parses the CSV and returns contacts
end
end
class JsonImporter
def import(path)
# parses the JSON and returns contacts
end
end

The client code decides which one to use based on the file extension:

importer =
case File.extname(path)
when ".csv" then CsvImporter.new
when ".json" then JsonImporter.new
else raise ArgumentError, "Unsupported format: #{File.extname(path)}"
end
contacts = importer.import(path)

This code works, and if it only exists in one place, it can stay just like that (remember: start with ugly code). The problem arrives when the same decision shows up in a second place. And it gets worse when a new format needs to be added, since that means hunting down every case statement to update it. Each of those spots also knows every single concrete importer class, when all it actually cares about is receiving something that responds to import.

Both symptoms together (a duplicated decision, and clients that know concrete classes) are the signal that creation deserves to be a responsibility with a name of its own.

Simple Factory: centralizing the decision

The first solution is to move the case statement to a single place. Here’s the complete code:

class CsvImporter
def initialize(path)
@path = path
end
def import
# parses the CSV and returns contacts
end
end
class JsonImporter
def initialize(path)
@path = path
end
def import
# parses the JSON and returns contacts
end
end
class ImporterFactory
def self.for(path)
case File.extname(path)
when ".csv" then CsvImporter.new(path)
when ".json" then JsonImporter.new(path)
else raise ArgumentError, "Unsupported format: #{File.extname(path)}"
end
end
end

And the client is reduced to what it actually cares about:

path = "contacts.csv"
contacts = ImporterFactory.for(path).import

Now the client no longer knows about CsvImporter or JsonImporter: all it knows is that the factory hands it back something that imports contacts. The decision lives in a single place.

Adding a new format means writing its class (new code) and adding a branch to the case statement. No client needs to change. And on top of that, the factory is easy to test on its own: given a path, does it return the right importer?

This solution is known as a Simple Factory. Don’t confuse it with the Gang of Four’s “Factory Method” pattern, which is a different thing altogether, as we’ll see below.

Using a hash of classes

Look at the factory’s case statement again. Each branch answers two different questions at once: which class corresponds to each extension (for instance, .csvCsvImporter), and how it’s looked up and instantiated. The result is that the mapping ends up buried inside a control structure, meaning that to find out which formats the application supports, you have to read logic.

In Ruby you can separate the mapping from the logic, taking advantage of the fact that classes are objects: you can store them as hash values, pass them as arguments, and send them messages. So the extension → class mapping can be expressed as data, while the logic is pulled into its own method:

class ImporterFactory
IMPORTERS = {
".csv" => CsvImporter,
".json" => JsonImporter
}.freeze
def self.for(path)
extension = File.extname(path)
importer_class = IMPORTERS.fetch(extension) do
raise ArgumentError, "Unsupported format: #{extension}"
end
importer_class.new(path)
end
end

Adding a format now costs the same as before (a new class, plus a new line in the hash), but the logic itself never changes. On top of that, fetch with a block gives us the unsupported-format error without writing a single conditional.

The Factory Method

The original Factory Method pattern from the Gang of Four is more specific. Its intent, per the book: define an interface for creating an object, letting subclasses decide which class to instantiate. The key part is subclasses. There’s no separate factory class in this pattern: the creation method lives inside a class that also does other things, and it’s the subclasses that override it.

Let’s look at it with a new example that imports contacts through these steps:

class ContactImport
def call(path)
contacts = importer(path).import
contacts
.reject { |contact| contact.email.nil? }
.sort_by(&:name)
end
private
# This is the "factory method"
def importer(_)
raise NotImplementedError, "#{self.class} must define importer"
end
end
class CsvContactImport < ContactImport
private
def importer(path)
CsvImporter.new(path)
end
end
class JsonContactImport < ContactImport
private
def importer(path)
JsonImporter.new(path)
end
end

The superclass defines the whole algorithm in call, but leaves a gap: the importer method, whose only job is to create the object. Each subclass fills that gap with its own concrete class, so the superclass uses the importer without ever knowing which one it is.

In other words, the Factory Method doesn’t answer “which class do I instantiate based on this parameter?” but rather “I have a process I want to inherit as-is, changing only which object gets created along the way.” It’s a pattern of specialization through inheritance, which is why it usually shows up alongside a Template Method (the fixed algorithm with gaps that subclasses fill in).

The dependency injection alternative

In practice, the classic Factory Method doesn’t see much use, because there’s an alternative that solves the same problem without creating a hierarchy. If the only thing that varies between subclasses is which object they create, you can inject that object directly:

class ContactImport
def initialize(importer:)
@importer = importer
end
def call
contacts = @importer.import
contacts
.reject { |contact| contact.email.nil? }
.sort_by(&:name)
end
end
json_importer = JsonImporter.new("file.json")
ContactImport.new(importer: json_importer).call

Three classes collapse into one, the variation is decided through composition instead of inheritance, and testing gets simpler too: to test call you no longer need test subclasses, just a fake importer.

That said, the classic pattern is more appropriate when the subclasses already exist for other reasons (because they override more than just creation).

Wrapping up

Creating objects is a responsibility, and when it involves choosing between concrete classes, it can be worth applying a pattern. The Simple Factory is a very convenient form, and it can be reduced to a hash of classes. The classic Factory Method is different: a creation gap in a hierarchy, filled in by subclasses, which can be replaced by composition through dependency injection.

What both approaches share is separating using from creating, so client code talks to interfaces while the decision about concrete types lives in a single place, ready for whatever change comes next.

Test your knowledge

  1. What's the underlying problem a factory solves?

  1. A class with a method that uses a case statement to decide which object to instantiate is...

  1. In Ruby, what makes it possible to replace a factory's case statement with a hash?

  1. What's the cost of having each importer register itself with the factory?

  1. When does the classic Factory Method pay off over injecting the dependency?