Abstract Factory
Creational Design Patterns
In the last chapter, we used the Factory Method to create a single Transport
object. But what if you need to create a whole set of related objects that are designed to work together?
Consider a cross-platform application that needs to support different UI themes, like Windows and macOS. A consistent theme requires that all UI elements match. You need a WindowsButton
, a WindowsCheckbox
, and a WindowsTextField
for the Windows theme. For macOS, you need a MacButton
, a MacCheckbox
, and a MacTextField
.
How can your application create a full UI without scattering if (os == 'windows')
logic everywhere? More importantly, how do you guarantee you don't accidentally create a MacCheckbox
in a Windows-themed window? The client code needs a way to say, "Give me the complete set of UI elements for the current theme," and be sure they are all from the same family.
A Factory for Factories
The Abstract Factory is a creational pattern that provides an interface for creating families of related or dependent objects without specifying their concrete classes.
The key word here is families. While the Factory Method deals with creating one type of product, the Abstract Factory deals with creating a suite of different products that are designed to be used together. You can think of it as a "factory for factories," where the Abstract Factory defines the contract for creating a whole set of related items.
The Mechanics: The Participants
This pattern has more moving parts than the Factory Method:
- Abstract Products: An interface for each distinct product in the family (e.g.,
IButton
,ICheckbox
). - Concrete Products: The concrete implementations for each product, for each family (e.g.,
WindowsButton
,MacButton
,WindowsCheckbox
,MacCheckbox
). - Abstract Factory: A single interface that defines a method for creating each of the abstract products (e.g.,
IGuiFactory
withcreateButton()
andcreateCheckbox()
methods). - Concrete Factories: A class for each product family that implements the
AbstractFactory
interface. It knows how to create all the products for one specific, consistent theme (e.g.,WindowsFactory
createsWindowsButton
andWindowsCheckbox
).
Example: The GUI Factory
Let's implement our cross-platform UI factory.
Step 1: Define the Abstract Products
public interface IButton { void paint(); }
public interface ICheckbox { void paint(); }
Step 2: Create the Concrete Product Families
// Windows family
public class WindowsButton implements IButton { /* ... */ }
public class WindowsCheckbox implements ICheckbox { /* ... */ }
// macOS family
public class MacButton implements IButton { /* ... */ }
public class MacCheckbox implements ICheckbox { /* ... */ }
Step 3: Define the Abstract Factory Interface
This is the contract for any factory that can create a full set of UI elements.
public interface IGuiFactory {
IButton createButton();
ICheckbox createCheckbox();
}
Step 4: Create the Concrete Factories
Each factory knows how to create a matched set of products.
public class WindowsFactory implements IGuiFactory {
@Override public IButton createButton() { return new WindowsButton(); }
@Override public ICheckbox createCheckbox() { return new WindowsCheckbox(); }
}
public class MacFactory implements IGuiFactory {
@Override public IButton createButton() { return new MacButton(); }
@Override public ICheckbox createCheckbox() { return new MacCheckbox(); }
}
Step 5: The Client is Configured with a Factory
The client code is configured with a specific factory at runtime. It is completely decoupled from the concrete UI elements and only works with the abstract interfaces.
public class Application {
private final IButton button;
private final ICheckbox checkbox;
public Application(IGuiFactory factory) {
// The application receives a factory and uses it to create its UI elements.
// It doesn't know the concrete types of the elements it's creating.
button = factory.createButton();
checkbox = factory.createCheckbox();
}
public void paint() {
button.paint();
checkbox.paint();
}
}
public class ApplicationConfigurator {
public static void main(String[] args) {
// Configuration can come from a file, environment variable, etc.
String osName = System.getProperty("os.name").toLowerCase();
IGuiFactory factory;
if (osName.contains("win")) {
factory = new WindowsFactory();
} else if (osName.contains("mac")) {
factory = new MacFactory();
} else {
throw new UnsupportedOperationException("Your OS is not supported.");
}
Application app = new Application(factory);
app.paint();
}
}
Interview Focus: Analysis and Trade-offs
Benefits:
- Guarantees Consistency: The primary benefit. Since a concrete factory creates a whole family of products, you are guaranteed that the products you get are from the same variant and are compatible with each other.
- Excellent for SOLID: It's a great example of OCP, as you can add a whole new family (e.g.,
LinuxFactory
) without changing the client code. It also promotes DIP by making the client depend on the abstract factory interface, not concrete factories. - Isolates Concrete Classes: The client code is completely decoupled from the concrete product implementations, leading to a highly flexible system.
Drawbacks:
- High Complexity: This is the main trade-off. The pattern introduces a lot of new interfaces and classes, which can make the codebase more complex if you only have a few product families.
- Difficult to Add New Products: While it's easy to add new families (like
LinuxFactory
), it is very difficult to add a new product type (like aITextField
). Doing so would require changing theIGuiFactory
interface, which would force you to modify all of your existing concrete factory classes. This is a critical point to raise in an interview.
How Abstract Factory Influences System Design
- Factory Method vs. Abstract Factory: This is the most common point of confusion and a great topic to clarify in an interview.
- Factory Method uses inheritance to let subclasses decide which single product to create.
- Abstract Factory uses composition (the client holds a factory object) to create families of related products.
- Often, the methods inside an Abstract Factory (
createButton()
,createCheckbox()
) are themselves Factory Methods.
- Singleton Pattern: The concrete factory classes (
WindowsFactory
,MacFactory
) are often implemented as Singletons. This makes sense because you typically only need one instance of each factory to create all the products for that family. - Real-World Use Cases: This pattern shines whenever you need to provide interchangeable "kits" of components. This is common for:
- UI Theming / Skinning: As in our example.
- Database Abstraction: Creating a family of
Connection
,Command
, andDataReader
objects for different database vendors (e.g., aSqlServerFactory
vs. anOracleFactory
). - Supporting Different Document Formats: Creating parsers and renderers for different file types like PDF, HTML, or DOCX.