Factory Method & Abstract Factory: Scaling Creation
Move beyond the Practical Factory. Learn how to use inheritance and interfaces to delegate object creation, ensuring your system follows SRP and OCP at scale.
In our Practical Factory lesson, we learned how to centralize if-else logic into a single class. But as your system grows, even a single Factory class can become a bottleneck or a violation of principles.
How do we scale object creation when we have dozens of types and complex dependencies? Enter the Factory Method and Abstract Factory patterns.
1. The Factory Method Pattern: "I Know Myself Best"
In the Practical Factory, the factory class holds the "mapping" (e.g., if season == WINTER return CROW). But what if the Season class itself could tell us which bird to create?
The Problem with Centralized Mapping
If you have a BirdFactory that maps seasons to birds, you have a separate class that needs to be updated every time a new season is added. If you forget to update the factory, your system breaks.
The Solution
Instead of a separate class with if-else, we add a Factory Method directly into the parent class or interface.
abstract class Season {
// This is the Factory Method
public abstract createHappiestBird(): Bird;
}
class Summer extends Season {
@Override
public createHappiestBird(): Bird {
return new Peacock();
}
}Why use this?
- Strict OCP: When you add a
Monsoonseason, you must implementcreateHappiestBird(). You don't need to touch any other class. - No If-Else: The client just calls
season.createHappiestBird(). Polymorphism handles the rest.
2. The Abstract Factory: Solving the SRP Violation
The Factory Method works great for one object. But what if a class needs to create multiple related objects?
Imagine a Database interface. It might need to create a Query, a Transaction, and a SecurityLayer. If you add createTransaction(), createQuery(), and createSecurity() methods to the Database interface, you are violating the Single Responsibility Principle (SRP). The database is now responsible for being a database AND being a factory for three different things.
The Solution: Extracting the Factory
We extract all those factory methods into a dedicated Abstract Factory interface.
interface Database {
connect(): void;
// Returns the specialized factory
createFactory(): DatabaseFactory;
}
interface DatabaseFactory {
createQuery(): Query;
createTransaction(): Transaction;
}Now, a MySQLDatabase will return a MySQLDatabaseFactory, which knows how to create MySQLQuery and MySQLTransaction.
Real-World Example: Cross-Platform UI (Flutter/React Native)
Think about building an app for both Android and iOS.
- On Android, a button should look like a material design button.
- On iOS, it should look like a Cupertino button.
Instead of writing if (platform == ANDROID) everywhere, Flutter uses an Abstract Factory.
- UI Interface: Has a method
getComponentFactory(). - ComponentFactory (Abstract Factory): Has methods like
createButton(),createMenu(), andcreateCheckbox(). - AndroidComponentFactory: Returns
AndroidButton,AndroidMenu. - iOSComponentFactory: Returns
iOSButton,iOSMenu.
The client code just says: ui.getComponentFactory().createButton().display(). It doesn't care which platform it's running on!
3. The "Factory Triad": Working Together
In a real-world system like a Database Driver, you often use all three patterns together. This is the Composite Journey of an Object:
- Practical Factory: You use a
DatabasePracticalFactory.getDatabase(url)to get the correct database object (e.g.,MySQLDatabase) based on a config string. - Factory Method: The
MySQLDatabaseobject has a factory methodcreateFactory()that returns a specialized factory. - Abstract Factory: The returned
MySQLDatabaseFactoryprovides the actualMySQLQueryorMySQLTransactionobjects you need.
The Client Traversal Path:
Database → DatabaseFactory → Query
This multi-stage handoff ensures that your main application logic is completely decoupled from the database vendor. You can swap MySQL for PostgreSQL just by changing a single string in your config file, and the rest of the chain handles itself!
Summary: Choosing Your Factory
| Feature | Practical Factory | Factory Method | Abstract Factory |
|---|---|---|---|
| Purpose | Simple object creation based on input. | Delegates creation to subclasses via inheritance. | Creates families of related objects. |
| Location | Separate class with static methods. | Method within the class hierarchy. | Separate interface for creational methods. |
| Scale | Best for small-scale projects. | Best for deep inheritance trees. | Best for complex, cross-platform libraries. |
Key Takeaway
While the Practical Factory is what you will explicitly "design" 90% of the time, the Factory Method and Abstract Factory often emerge naturally as you refine your architecture to follow SRP and OCP. They are the tools you use when your "Simple Factory" starts becoming too complex.
Further Reading
- Refactoring Guru: Abstract Factory
- Case Study: Explore the JDBC (Java Database Connectivity) API to see these patterns in action across dozens of different database vendors.
With this, we conclude our deep dive into the Factory family. In our next module, we will explore the Prototype & Registry Pattern, where we learn how to create perfect copies of objects without knowing their specific types.
Practice what you just read.
Keep reading