Factory Design Patterns in Java
Factory Design Patterns in Java
In Java, everything is an object that needs to be instantiated. When you use direct new to create objects, you introduce tight coupling throughout your codebase. If you need to replace an object implementation, you'd have to modify every location where new was used, clearly violating the Open-Closed Principle. By using a factory to produce objects, you only interact with the factory, completely decoupling from concrete object implementations. To switch implementations, you simply modify the factory, achieving loose coupling. This is the primary advantage of the factory pattern.
Simple Factory Pattern
Structure
The Simple Factory pattern includes the following components:
- Abstract Product: Defines the product specification and describes the main characteristics and functionality
- Concrete Product: Implements or extends the abstract product
- Concrete Factory: Provides the method for creating products; clients obtain product instances through this method
Implementation
Let's improve the above scenario using the Simple Factory pattern.
public abstract class Beverage {
public abstract String getName();
public void addSweetener() {
System.out.println("Adding sweetener");
}
public void addCream() {
System.out.println("Adding cream");
}
}
public class Espresso extends Beverage {
@Override
public String getName() {
return "Espresso";
}
}
public class Cappuccino extends Beverage {
@Override
public String getName() {
return "Cappuccino";
}
}
public class CafeService {
public Beverage serveBeverage(String type) {
SimpleBeverageFactory factory = new SimpleBeverageFactory();
Beverage beverage = factory.produceBeverage(type);
beverage.addCream();
beverage.addSweetener();
return beverage;
}
}
public class SimpleBeverageFactory {
public Beverage produceBeverage(String type) {
Beverage beverage = null;
if ("espresso".equals(type)) {
beverage = new Espresso();
} else if ("cappuccino".equals(type)) {
beverage = new Cappuccino();
} else {
throw new IllegalArgumentException("Invalid beverage type");
}
return beverage;
}
}
The factory handles object creation details. Once you have the SimpleBeverageFactory, the serveBeverage() method in CafeService becomes a client of this factory. Later, when you need Beverage objects, you simply obtain them from the factory, eliminating the coupling with Beverage implementations. How ever, this creates a new coupling between CafeService and SimpleBeverageFactory, as well as between the factory and concrete products.
If you need to add new beverage varieties in the future, you will have to modify the SimpleFactory code, which violates the Open-Closed Principle. Since factory clients may be numerous (such as various delivery apps), modifying only the factory code saves substantial effort across the codebase.
Advantages and Disadvantages
Advantages
- Encapsulates object creation logic; you can retrieve objects directly through parameters
- Separates object creation from business logic, avoiding modifications to client code
- New products can be implemented by modifying the factory class without touching source code
- Reduces the likelihood of client code modifications and makes extensions easier
Disadvantages
- Adding new products still requires modifying the factory class, violating the Open-Closed Principle
Simple Factory Extension - Static Factory
For other clients, if you want to obtain objects from the factory, you don't need to create a factory instance anymore.
public class StaticBeverageFactory {
public static Beverage produceBeverage(String type) {
Beverage beverage = null;
if ("espresso".equals(type)) {
beverage = new Espresso();
} else if ("cappuccino".equals(type)) {
beverage = new Cappuccino();
} else {
throw new IllegalArgumentException("Invalid beverage type");
}
return beverage;
}
}
Factory Method Pattern
The Factory Method pattern solves the shortcomings mentioned above and fully adheres to the Open-Closed Principle.
Concept
Define an interface for creating objects, letting subclasses decide which product class to instantiate. Factory Method defers product instantiation to its factory subclasses.
Structure
The Factory Method pattern consists of:
- Abstract Factory: Provides the interface for creating products; clients access concrete factories through it to create products
- Concrete Factory: Implements abstract factory methods to create specific products
- Abstract Product: Defines product specifications, describing main characteristics and functionality
- Concrete Product: Implements the abstract product interface, created by concrete factories, with a one-to-one correspondence to concrete factories
Implementation
Abstract Factory:
public interface BeverageFactory {
Beverage createBeverage();
}
Concrete Factories:
public class CappuccinoFactory implements BeverageFactory {
public Beverage createBeverage() {
return new Cappuccino();
}
}
public class EspressoFactory implements BeverageFactory {
public Beverage createBeverage() {
return new Espresso();
}
}
Cafe Service:
public class CafeService {
private BeverageFactory factory;
public CafeService(BeverageFactory factory) {
this.factory = factory;
}
public Beverage serveBeverage(String type) {
Beverage beverage = factory.createBeverage();
beverage.addCream();
beverage.addSweetener();
return beverage;
}
}
Advantages and Disadvantages
Advantages
- Users only need to know the specific factory name to get the desired product without understanding the product creation process
- Adding new products only requires adding concrete product classes and corresponding concrete factory classes, without modifying existing factories, satisfying the Open-Closed Principle
Disadvantages
- Adding each product requires adding a concrete product class and a corresponding concrete factory class, increasing system complexity
Abstract Factory Pattern
Consider multi-level product production: products from the same concrete factory belonging to different levels constitute a product family.
Concept
- Provides an interface for creating a group of related or interdependent objects for the client, so that clients can obtain products of different levels from the same family without specifying concrete product classes.
- Abstract Factory is the upgraded version of Factory Method. Factory Method produces only one level of products, while Abstract Factory can produce multiple levels of products.
Structure
- Abstract Factory: Provides the interface for creating products, containing multiple product creation methods that can create multiple products of different levels for a product family.
- Concrete Factory: Mainly implements multiple abstract methods in the Abstract Factory to complete specific product creation.
- Abstract Product: Defines product specifications, describing main characteristics and functionality. Abstract Factory has multiple abstract products.
- Concrete Product: Implements the Abstract Product interface, created by Concrete Factory, with a many-to-one relationship with Concrete Factory.
Implementation
Imagine the cafe business changes: not only beverages but also pastries like Tiramisu and Matcha Mousse need to be produced. Following the Factory Method pattern, you would need to define Tiramisu class, Matcha Mousse class, Tiramisu factory, Matcha Mousse factory, and Pastry factory, easily causing class explosion. Cappuccino and Espresso belong to one product level (both are coffee); Tiramisu and Matcha Mousse belong to another product level (pastries). Cappuccino and Tiramisu are in the same product family (Italian style), while Espresso and Matcha Mousse are in the same product family (American style). This scenario is ideal for the Abstract Factory pattern.
Abstract Factory:
public interface PastryFactory {
Beverage createBeverage();
Pastry createPastry();
}
Concrete Factories:
public class AmericanPastryFactory implements PastryFactory {
public Beverage createBeverage() {
return new Espresso();
}
public Pastry createPastry() {
return new MatchaMousse();
}
}
public class ItalianPastryFactory implements PastryFactory {
public Beverage createBeverage() {
return new Cappuccino();
}
public Pastry createPastry() {
return new Tiramisu();
}
}
To add a new product family, you only need to add a corresponding factory class without modifying any existing classes.
Advantages and Disadvantages
Advantages
When multiple objects within a product family are designed to work together, it ensures clients always use objects from the same product family.
Disadvantages
When a new product needs to be added to a product family, all factory classes need modification.
Use Cases
- When the objects to create form a series of interrelated or interdependent product families, such as TV, washing machine, and air conditioner in an appliance factory.
- When the system has multiple product families but only one product family is used at a time, such as someone who only likes clothes and shoes from a specific brand.
- When the system provides a product library with all products having the same interface, and clients don't depend on product instance creation details or internal structures.
Examples: changing input method skins where the entire set changes together; generating programs for different operating systems.
Factory Pattern Extension
Simple Factory + Configuration File to Decouple
The Factory pattern combined with configuration files decouples factory objects from product objects. The factory class loads fully qualified class names from configuration files and creates objects through reflection, loading once and storing for reuse. Clients can directly retrieve objects when needed.
Step 1: Define Configuration File
For demonstration, we use a properties file named bean.properties:
espresso=com.example.pattern.factory.config_factory.Espresso
cappuccino=com.example.pattern.factory.config_factory.Cappuccino
Step 2: Improve Factory Class
public class BeverageFactory {
private static HashMap<String, Beverage> registry = new HashMap<>();
static {
Properties config = new Properties();
InputStream input = BeverageFactory.class.getClassLoader()
.getResourceAsStream("bean.properties");
try {
config.load(input);
for (Object key : config.keySet()) {
String className = config.getProperty((String) key);
Class<?> clazz = Class.forName(className);
Beverage beverage = (Beverage) clazz.newInstance();
registry.put((String) key, beverage);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static Beverage produceBeverage(String name) {
return registry.get(name);
}
}
The static member variable stores created objects (key stores the name, value stores the corresponding object). Reading the configuration file and creating objects are placed in static blocks to ensure execution only happens once.
JDK Source Code Analysis - Iterator Pattern
The Collection interface acts as the abstract factory, with ArrayList as the concrete factory. The Iterator interface serves as the abstract product, while the InnerIter class within ArrayList acts as the concrete product. The iterator() method in the concrete factory creates instances of concrete product classes.
Additional examples:
- The
getInstance()method in DateFormat class uses the factory pattern.- The
getInstance()method in Calendar class uses the factory pattern.