Factory Method Pattern
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 endend
class JsonImporter def import(path) # parses the JSON and returns contacts endendThe 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 endend
class JsonImporter def initialize(path) @path = path end
def import # parses the JSON and returns contacts endend
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 endendAnd the client is reduced to what it actually cares about:
path = "contacts.csv"
contacts = ImporterFactory.for(path).importNow 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, .csv → CsvImporter), 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) endendAdding 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:
- Import contacts from the file
- Discard contacts with no email
- Sort by name
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" endend
class CsvContactImport < ContactImport private
def importer(path) CsvImporter.new(path) endend
class JsonContactImport < ContactImport private
def importer(path) JsonImporter.new(path) endendThe 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) endend
json_importer = JsonImporter.new("file.json")
ContactImport.new(importer: json_importer).callThree 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
-
What's the underlying problem a factory solves?
-
A class with a method that uses a
casestatement to decide which object to instantiate is...
-
In Ruby, what makes it possible to replace a factory's
casestatement with a hash?
-
What's the cost of having each importer register itself with the factory?
-
When does the classic Factory Method pay off over injecting the dependency?