15 Classic Design Patterns Explained, Java Implementation & Best Practices Guide
9/3/25...About 5 min
15 Classic Design Patterns Explained, Java Implementation & Best Practices Guide
Design patterns are the "secret techniques" of software engineering, elegant solutions to common problems summarized by predecessors through countless practices. They make your code more robust, flexible, and easier to extend.
I. Creational Patterns
The core of these patterns is how to create objects. They separate the object creation process from the usage process, making your system more flexible when creating objects.
1. Singleton Pattern
- Purpose: Ensure a class has only one instance throughout the entire application and provide a global access point.
- Implementation: Privatize the constructor and provide a public static method to get the unique instance.
public class ConfigurationManager {
private static ConfigurationManager instance;
private ConfigurationManager() {}
public static ConfigurationManager getInstance() {
if (instance == null) {
instance = new ConfigurationManager();
}
return instance;
}
}
2. Factory Method Pattern
- Purpose: Define an interface for creating objects, but let subclasses decide which class to instantiate.
- Implementation: Abstract factory interface defines creation methods, each concrete subclass factory is responsible for creating one specific product.
interface Car { void drive(); }
class Audi implements Car { public void drive() { System.out.println("Driving Audi."); } }
interface CarFactory { Car createCar(); }
class AudiFactory implements CarFactory {
public Car createCar() { return new Audi(); }
}
3. Abstract Factory Pattern
- Purpose: Create a family of related or dependent objects without specifying their concrete classes.
- Implementation: Define an abstract factory interface containing multiple methods for creating different types of products.
// Abstract products
interface Button { void render(); }
interface Checkbox { void render(); }
// Concrete products
class WinButton implements Button { public void render() { System.out.println("Rendering a Windows Button."); } }
class MacButton implements Button { public void render() { System.out.println("Rendering a Mac Button."); } }
// Abstract factory
interface GUIFactory {
Button createButton();
Checkbox createCheckbox();
}
// Concrete factory
class WindowsFactory implements GUIFactory {
public Button createButton() { return new WinButton(); }
public Checkbox createCheckbox() { return new WinCheckbox(); }
}
4. Builder Pattern
- Purpose: Separate the construction of a complex object from its representation, so that the same construction process can create different representations.
- Implementation: Define a builder interface with step-by-step construction methods and a method to get the final product.
class Computer {
private String cpu;
private String ram;
private String storage;
// ...getters and setters...
}
interface ComputerBuilder {
void buildCPU();
void buildRAM();
void buildStorage();
Computer getResult();
}
class GamingComputerBuilder implements ComputerBuilder {
private Computer computer = new Computer();
public void buildCPU() { computer.setCpu("Intel Core i9"); }
public void buildRAM() { computer.setRam("32GB DDR5"); }
public void buildStorage() { computer.setStorage("1TB SSD"); }
public Computer getResult() { return computer; }
}
II. Structural Patterns
These patterns focus on how to compose classes and objects to form larger structures and make them work together.
5. Adapter Pattern
- Purpose: Allow incompatible objects to work together.
- Implementation: Create an "adapter" class that implements the desired interface and internally holds the incompatible object.
interface MediaPlayer { void play(String audioType, String fileName); }
class AdvancedMediaPlayer { public void playMp4(String fileName) { System.out.println("Playing mp4 file: " + fileName); } }
class MediaAdapter implements MediaPlayer {
AdvancedMediaPlayer advancedPlayer;
public MediaAdapter() { this.advancedPlayer = new AdvancedMediaPlayer(); }
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("mp4")) {
advancedPlayer.playMp4(fileName);
}
}
}
6. Facade Pattern
- Purpose: Provide a simplified unified interface for a complex subsystem.
- Implementation: Create a facade class that contains references to multiple classes in the subsystem and provides a simple method to call these subsystems.
class Amplifier { public void turnOn() { System.out.println("Amplifier on."); } }
class DvdPlayer { public void turnOn() { System.out.println("DVD Player on."); } }
class Lights { public void dim() { System.out.println("Lights dimmed."); } }
class HomeTheaterFacade {
Amplifier amp;
DvdPlayer dvd;
Lights lights;
// constructor...
public void watchMovie() {
lights.dim();
amp.turnOn();
dvd.turnOn();
System.out.println("Movie is playing...");
}
}
7. Decorator Pattern
- Purpose: Dynamically add new functionality to objects without changing their structure.
- Implementation: Create an abstract decorator class that inherits from the decorated object's parent class and holds a reference to the decorated object. Concrete decorator subclasses are responsible for adding new functionality.
interface Coffee { double getCost(); String getDescription(); }
class SimpleCoffee implements Coffee {
public double getCost() { return 10; }
public String getDescription() { return "Simple coffee"; }
}
abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) { this.decoratedCoffee = coffee; }
public double getCost() { return decoratedCoffee.getCost(); }
public String getDescription() { return decoratedCoffee.getDescription(); }
}
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) { super(coffee); }
public double getCost() { return super.getCost() + 2.0; }
public String getDescription() { return super.getDescription() + ", with milk"; }
}
8. Composite Pattern
- Purpose: Compose objects into tree structures to represent part-whole hierarchies.
- Implementation: Define a component interface that can be either a leaf node (simple object) or a container node (containing other components).
import java.util.ArrayList;
import java.util.List;
interface Component { void showInfo(); }
class File implements Component {
private String name;
public File(String name) { this.name = name; }
public void showInfo() { System.out.println("File: " + name); }
}
class Directory implements Component {
private String name;
private List<Component> components = new ArrayList<>();
public Directory(String name) { this.name = name; }
public void add(Component c) { components.add(c); }
public void showInfo() {
System.out.println("Directory: " + name);
for (Component c : components) {
c.showInfo();
}
}
}
9. Proxy Pattern
- Purpose: Provide a placeholder or proxy for another object to control access to it.
- Implementation: The proxy class and the proxied class implement the same interface, the proxy class holds a reference to the proxied class and controls calls to it.
interface Image { void display(); }
class RealImage implements Image {
private String filename;
public RealImage(String filename) { this.filename = filename; loadFromDisk(); }
private void loadFromDisk() { System.out.println("Loading " + filename); }
public void display() { System.out.println("Displaying " + filename); }
}
class ProxyImage implements Image {
private RealImage realImage;
private String filename;
public ProxyImage(String filename) { this.filename = filename; }
public void display() {
if (realImage == null) {
realImage = new RealImage(filename);
}
realImage.display();
}
}
III. Behavioral Patterns
These patterns focus on responsibility assignment and algorithms between objects, describing how objects communicate and collaborate with each other.
10. Iterator Pattern
- Purpose: Provide a way to sequentially access elements in an aggregate object without exposing its internal representation.
- Implementation: Define an iterator interface containing
hasNext()
andnext()
methods.
import java.util.List;
import java.util.ArrayList;
interface MyIterator { boolean hasNext(); Object next(); }
class BookShelfIterator implements MyIterator {
private List<String> books;
private int index = 0;
public BookShelfIterator(List<String> books) { this.books = books; }
public boolean hasNext() { return index < books.size(); }
public Object next() { return books.get(index++); }
}
11. Observer Pattern
- Purpose: Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
- Implementation: The "subject" maintains a list of "observers" and notifies them by traversing the list when state changes.
import java.util.ArrayList;
import java.util.List;
interface Observer { void update(String message); }
interface Subject { void attach(Observer o); void notifyObservers(String message); }
class ConcreteSubject implements Subject {
private List<Observer> observers = new ArrayList<>();
public void attach(Observer o) { observers.add(o); }
public void notifyObservers(String message) {
for (Observer o : observers) {
o.update(message);
}
}
}
12. Strategy Pattern
- Purpose: Define a family of algorithms, encapsulate them and make them interchangeable.
- Implementation: Define a "strategy" interface, each concrete strategy class implements one algorithm.
interface PaymentStrategy { void pay(double amount); }
class AlipayStrategy implements PaymentStrategy {
public void pay(double amount) { System.out.println("Paying " + amount + " with Alipay."); }
}
class PaymentContext {
private PaymentStrategy strategy;
public void setPaymentStrategy(PaymentStrategy s) { this.strategy = s; }
public void executePayment(double amount) { strategy.pay(amount); }
}
13. Command Pattern
- Purpose: Encapsulate a request as an object, thereby allowing you to parameterize clients with different requests.
- Implementation: Define a command interface containing an
execute()
method. Concrete command classes implement this interface and hold receiver objects.
// Command interface
interface Command { void execute(); }
// Receiver: the object that actually performs the operation
class Light {
public void turnOn() { System.out.println("Light is on."); }
public void turnOff() { System.out.println("Light is off."); }
}
// Concrete command: encapsulates the request
class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) { this.light = light; }
public void execute() { light.turnOn(); }
}
14. State Pattern
- Purpose: Allow an object to alter its behavior when its internal state changes.
- Implementation: Encapsulate the object's behavior into different state classes and let the object hold a reference to the current state.
interface VendingMachineState { void selectProduct(); void dispenseProduct(); }
class NoProductState implements VendingMachineState {
public void selectProduct() { System.out.println("Product selected."); }
public void dispenseProduct() { System.out.println("No product to dispense."); }
}
class HasProductState implements VendingMachineState {
public void selectProduct() { System.out.println("Product already selected."); }
public void dispenseProduct() { System.out.println("Dispensing product..."); }
}
15. Template Method Pattern
- Purpose: Define the skeleton of an algorithm in an operation, deferring some steps to subclasses.
- Implementation: Define a template method in an abstract parent class that calls some abstract "hook" methods, which are implemented by subclasses.
abstract class HouseBuilder {
public final void buildHouse() {
buildFoundation();
buildWalls();
buildRoof();
}
protected abstract void buildFoundation();
protected abstract void buildWalls();
protected abstract void buildRoof();
}
class WoodHouseBuilder extends HouseBuilder {
protected void buildFoundation() { System.out.println("Building wooden foundation."); }
protected void buildWalls() { System.out.println("Building wooden walls."); }
protected void buildRoof() { System.out.println("Building wooden roof."); }
}