Advanced Java Design Patterns: Factory Pattern
Introduction
Building upon the singleton pattern explored previously, this article examines the factory pattern—a fundamental creational design pattern. It encompasses three variations: simple factory, factory method, and abstract factory patterns.
Simple Factory Pattern
The simple factory pattern falls under the category of creational patterns, also known as the static factory method pattern. This approach delegates the responsibility of creating specific product instances to a single factory object. Clients merely specify the desired product type, and the factory returns an appropriate instance.
Consider the analogy of gaming on a computer: users simply indicate which game they want to launch, and the system opens it without requiring knowledge of its internal workings.
Below is an implementation example:
public class GameLauncher {
private static final String LOBSTER = "LOBSTER";
private static final String DOTA = "DOTA";
public static void main(String[] args) {
Game game1 = GameFactory.createGame(LOBSTER);
Game game2 = GameFactory.createGame(DOTA);
game1.start();
game2.start();
}
}
interface Game {
void start();
}
class LobsterGame implements Game {
@Override
public void start() {
System.out.println("Playing Lobster...");
}
}
class DotaGame implements Game {
@Override
public void start() {
System.out.println("Playing Dota...");
}
}
class GameFactory {
private static final String LOBSTER = "LOBSTER";
private static final String DOTA = "DOTA";
public static Game createGame(String gameType) {
if (LOBSTER.equalsIgnoreCase(gameType)) {
return new LobsterGame();
} else if (DOTA.equalsIgnoreCase(gameType)) {
return new DotaGame();
}
return null;
}
}
Output:
Playing Lobster...
Playing Dota...
By employing the simple factory pattern, instantiation logic is encapsulated within the factory, hiding implementation details from clients. This simplifies usage and facilitates switching between products by modifying only the factory input. However, extending the system with new types requires updating the factory's conditional logic, violating the open-closed principle when dealing with many new types.
Factory Method Pattern
The factory method pattern is one of the most commonly used design patterns in Java and belongs to the creational category. It defines an interface for creating objects but allows subclasses to determine which class to instantiate. This delays object creation to subclasses.
In contrast to the simple factory, the factory method pattern resolves the issue of tight coupling when adding new product types. Each product has its dedicated factory, reducing dependencies.
Here’s how the revised code looks:
public class GameLauncher {
private static final String LOBSTER = "LOBSTER";
private static final String DOTA = "DOTA";
private static final String WAR = "WAR";
public static void main(String[] args) {
Game game1 = new LobsterFactory().createGame();
Game game2 = new DotaFactory().createGame();
Game game3 = new WarFactory().createGame();
game1.start();
game2.start();
game3.start();
}
}
interface Game {
void start();
}
class LobsterGame implements Game {
@Override
public void start() {
System.out.println("Playing Lobster...");
}
}
class DotaGame implements Game {
@Override
public void start() {
System.out.println("Playing Dota...");
}
}
class WarGame implements Game {
@Override
public void start() {
System.out.println("Playing War...");
}
}
interface GameFactory {
Game createGame();
}
class LobsterFactory implements GameFactory {
@Override
public Game createGame() {
return new LobsterGame();
}
}
class DotaFactory implements GameFactory {
@Override
public Game createGame() {
return new DotaGame();
}
}
class WarFactory implements GameFactory {
@Override
public Game createGame() {
return new WarGame();
}
}
Output:
Playing Lobster...
Playing Dota...
Playing War...
The factory method pattern improves clarity and extensibility. Adding a new product involves creating a new factory class. However, this increases system complexity, as each product requires a corresponding factory class.
This pattern is widely used in frameworks like Hibernate for database dialect selection. If object creation is straightforward, using this pattern may introduce unnecessary copmlexity.
Abstract Factory Pattern
The abstract factory pattern creates families of related or dependent objects without specifying their concrete classes. It provides an interface for creating families of related or dependent objects.
Compared to the factory method, the abstract factory is more complex but offers better scalability. It groups related products into families and allows clients to work with entire families of objects.
Continuing the gaming example, we can categorize games into two families: PvP (Player vs Player) and PvE (Player vs Environment):
public class GameLauncher {
private static final String LOBSTER = "LOBSTER";
private static final String DOTA = "DOTA";
private static final String WAR = "WAR";
public static void main(String[] args) {
GameFactory factory1 = new PvPFamilyFactory();
factory1.createGame().start();
factory1.createGame2().start();
GameFactory factory2 = new PvEFamilyFactory();
factory2.createGame().start();
factory2.createGame2().start();
}
}
interface Game {
void start();
}
class LobsterGame implements Game {
@Override
public void start() {
System.out.println("Playing Lobster...");
}
}
class DotaGame implements Game {
@Override
public void start() {
System.out.println("Playing Dota...");
}
}
class WarGame implements Game {
@Override
public void start() {
System.out.println("Playing War...");
}
}
interface GameFactory {
Game createGame();
Game createGame2();
}
class PvPFamilyFactory implements GameFactory {
@Override
public Game createGame() {
return new LobsterGame();
}
@Override
public Game createGame2() {
return new WarGame();
}
}
class PvEFamilyFactory implements GameFactory {
@Override
public Game createGame() {
return new DotaGame();
}
@Override
public Game createGame2() {
return new WarGame();
}
}
Output:
Playing Lobster...
Playing War...
Playing Dota...
Playing War...
With the abstract factory pattern, clients interact with families of related products without needing to know their specific implementations. While this enhances usability, it makes adding entirely new product families more challenging. Adding a new product requires implementing both new interfaces and concrete classes.
Conclusion
Each factory pattern variant addresses different aspects of object creation and system design. Choosing among them depends on the level of abstraction reqiured and the expected scope of future extensions.