Introduction to Design Patterns
Design patterns in programming are typical solutions to common problems in software design. They are like blueprints that can be customized to solve a particular design issue in your code. Originating from the book “Design Patterns: Elements of Reusable Object-Oriented Software” by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, these patterns have become essential tools for developers.
Benefits of Using Design Patterns
Design patterns offer numerous advantages, making them a staple in modern software development. They promote code reusability, which reduces redundancy and saves time. Patterns also enhance code maintenance by providing a clear structure, making it easier to understand, modify, and debug. Additionally, they improve efficiency by offering tested, proven development paradigms.
Types of Design Patterns
Design patterns are classified into three main categories: Creational, Structural, and Behavioral. Each category addresses different aspects of a software problem.
Creational Design Patterns
Creational patterns focus on object creation mechanisms, aiming to create objects in a manner suitable for the situation. They abstract the instantiation process, making a system independent of how its objects are created, composed, and represented.
Singleton Pattern
The Singleton pattern ensures a class has only one instance and provides a global point of access to it. This is useful when exactly one object is needed to coordinate actions across the system.
Use Cases: Configuration management, logging, and access to resources such as printers.
Implementation:
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
Factory Method Pattern
The Factory Method pattern defines an interface for creating an object, but lets subclasses alter the type of objects that will be created.
Use Cases: When a class cannot anticipate the class of objects it must create or wants its subclasses to specify the objects.
Implementation:
abstract class Product {
abstract void operation();
}
class ConcreteProductA extends Product {
void operation() {
System.out.println("Operation of ConcreteProductA");
}
}
abstract class Creator {
abstract Product factoryMethod();
void someOperation() {
Product product = factoryMethod();
product.operation();
}
}
class ConcreteCreatorA extends Creator {
Product factoryMethod() {
return new ConcreteProductA();
}
}
Abstract Factory Pattern
The Abstract Factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes.
Use Cases: When the system needs to be independent of how its products are created and represented or when it needs to work with multiple families of products.
Implementation:
interface GUIFactory {
Button createButton();
Checkbox createCheckbox();
}
class WinFactory implements GUIFactory {
public Button createButton() {
return new WinButton();
}
public Checkbox createCheckbox() {
return new WinCheckbox();
}
}
Builder Pattern
The Builder pattern separates the construction of a complex object from its representation so that the same construction process can create different representations.
Use Cases: When the construction process of an object is complex or needs to allow different representations for the object.
Implementation:
class Product {
private String partA;
private String partB;
public void setPartA(String partA) {
this.partA = partA;
}
public void setPartB(String partB) {
this.partB = partB;
}
}
abstract class Builder {
protected Product product = new Product();
abstract void buildPartA();
abstract void buildPartB();
Product getResult() {
return product;
}
}
class ConcreteBuilder extends Builder {
void buildPartA() {
product.setPartA("PartA built by ConcreteBuilder");
}
void buildPartB() {
product.setPartB("PartB built by ConcreteBuilder");
}
}
class Director {
private Builder builder;
public Director(Builder builder) {
this.builder = builder;
}
public void construct() {
builder.buildPartA();
builder.buildPartB();
}
}
Prototype Pattern
The Prototype pattern is used to create a duplicate object while keeping performance in mind. This pattern involves implementing a prototype interface which tells to create a clone of the current object.
Use Cases: When the cost of creating a new object is expensive or complicated.
Implementation:
class Prototype implements Cloneable {
String name;
Prototype(String name) {
this.name = name;
}
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "Prototype{name='" + name + "'}";
}
}
public class PrototypeDemo {
public static void main(String[] args) {
try {
Prototype prototype1 = new Prototype("Prototype1");
Prototype prototype2 = (Prototype) prototype1.clone();
System.out.println(prototype1);
System.out.println(prototype2);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
Structural Design Patterns
Structural patterns deal with object composition or the way to assemble objects to realize new functionalities.
Adapter Pattern
The Adapter pattern allows incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces.
Use Cases: When an existing class’s interface does not match the one you need.
Implementation:
interface Target {
void request();
}
class Adaptee {
void specificRequest() {
System.out.println("Specific request");
}
}
class Adapter implements Target {
private Adaptee adaptee;
public Adapter(Adaptee adaptee) {
this.adaptee = adaptee;
}
public void request() {
adaptee.specificRequest();
}
}
Composite Pattern
The Composite pattern allows you to compose objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions of objects uniformly.
Use Cases: When you need to represent part-whole hierarchies of objects.
Implementation:
abstract class Component {
abstract void operation();
}
class Leaf extends Component {
void operation() {
System.out.println("Leaf operation");
}
}
class Composite extends Component {
private List<Component> children = new ArrayList<>();
void add(Component component) {
children.add(component);
}
void remove(Component component) {
children.remove(component);
}
void operation() {
for (Component child : children) {
child.operation();
}
}
}
Proxy Pattern
The Proxy pattern provides a surrogate or placeholder for another object to control access to it.
Use Cases: When you need to control access to an object or manage its lifecycle.
Implementation:
interface Subject {
void request();
}
class RealSubject implements Subject {
public void request() {
System.out.println("RealSubject: Handling request.");
}
}
class Proxy implements Subject {
private RealSubject realSubject;
public Proxy(RealSubject realSubject) {
this.realSubject = realSubject;
}
public void request() {
if (checkAccess()) {
realSubject.request();
logAccess();
}
}
private boolean checkAccess() {
System.out.println("Proxy: Checking access prior to firing a real request.");
return true;
}
private void logAccess() {
System.out.println("Proxy: Logging the time of request.");
}
}
Flyweight Pattern
The Flyweight pattern minimizes memory usage by sharing as much data as possible with other similar objects.
Use Cases: When you need to create a large number of similar objects efficiently.
Implementation:
import java.util.HashMap;
import java.util.Map;
interface Flyweight {
void operation(String extrinsicState);
}
class ConcreteFlyweight implements Flyweight {
private String intrinsicState;
public ConcreteFlyweight(String intrinsicState) {
this.intrinsicState = intrinsicState;
}
public void operation(String extrinsicState) {
System.out.println("Intrinsic State = " + intrinsicState + ", Extrinsic State = " + extrinsicState);
}
}
class FlyweightFactory {
private Map<String, Flyweight> flyweights = new HashMap<>();
public Flyweight getFlyweight(String key) {
if (!flyweights.containsKey(key)) {
flyweights.put(key, new ConcreteFlyweight(key));
}
return flyweights.get(key);
}
}
Decorator Pattern
The Decorator pattern allows behavior to be added to individual objects, dynamically, without affecting the behavior of other objects from the same class.
Use Cases: When you need to add responsibilities to objects dynamically and transparently.
Implementation:
interface Component {
String operation();
}
class ConcreteComponent implements Component {
public String operation() {
return "ConcreteComponent";
}
}
abstract class Decorator implements Component {
protected Component component;
public Decorator(Component component) {
this.component = component;
}
public String operation() {
return component.operation();
}
}
class ConcreteDecoratorA extends Decorator {
public ConcreteDecoratorA(Component component) {
super(component);
}
public String operation() {
return "ConcreteDecoratorA(" + super.operation() + ")";
}
}
Behavioral Design Patterns
Behavioral patterns are concerned with algorithms and the assignment of responsibilities between objects.
Observer Pattern
The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
Use Cases: When an object needs to notify other objects without knowing who they are.
Implementation:
interface Observer {
void update();
}
class ConcreteObserver implements Observer {
private String observerState;
@Override
public void update() {
this.observerState = "Updated";
System.out.println("Observer State: " + observerState);
}
}
class Subject {
private List<Observer> observers = new ArrayList<>();
void attach(Observer observer) {
observers.add(observer);
}
void detach(Observer observer) {
observers.remove(observer);
}
void notifyObservers() {
for (Observer observer : observers) {
observer.update();
}
}
}
Strategy Pattern
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
Use Cases: When a class needs to use different variants of an algorithm.
Implementation:
interface Strategy {
void execute();
}
class ConcreteStrategyA implements Strategy {
public void execute() {
System.out.println("Strategy A");
}
}
class Context {
private Strategy strategy;
public Context(Strategy strategy) {
this.strategy = strategy;
}
public void executeStrategy() {
strategy.execute();
}
}
Command Pattern
The Command pattern encapsulates a request as an object, thereby allowing for parameterization of clients with queues, requests, and operations.
Use Cases: When you need to parameterize objects by an action to perform or specify, queue, and execute requests at different times.
Implementation:
interface Command {
void execute();
}
class Light {
public void on() {
System.out.println("Light is on");
}
public void off() {
System.out.println("Light is off");
}
}
class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.on();
}
}
class LightOffCommand implements Command {
private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
public void execute() {
light.off();
}
}
class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
}
}
State Pattern
The State pattern allows an object to alter its behavior when its internal state changes. The object will appear to change its class.
Use Cases: When an object needs to change its behavior when its state changes.
Implementation:
Java:
interface State {
void doAction(Context context);
}
class StartState implements State {
public void doAction(Context context) {
System.out.println("Player is in start state");
context.setState(this);
}
public String toString() {
return "Start State";
}
}
class StopState implements State {
public void doAction(Context context) {
System.out.println("Player is in stop state");
context.setState(this);
}
public String toString() {
return "Stop State";
}
}
class Context {
private State state;
public void setState(State state) {
this.state = state;
}
public State getState() {
return state;
}
}
Chain of Responsibility Pattern
The Chain of Responsibility pattern is used to process varied requests, each of which may be dealt with by a different handler.
Java Implementation:
abstract class Logger {
public static int INFO = 1;
public static int DEBUG = 2;
public static int ERROR = 3;
protected int level;
protected Logger nextLogger;
public void setNextLogger(Logger nextLogger) {
this.nextLogger = nextLogger;
}
public void logMessage(int level, String message) {
if (this.level <= level) {
write(message);
}
if (nextLogger != null) {
nextLogger.logMessage(level, message);
}
}
abstract protected void write(String message);
}
class ConsoleLogger extends Logger {
public ConsoleLogger(int level) {
this.level = level;
}
protected void write(String message) {
System.out.println("Standard Console::Logger: " + message);
}
}
class ErrorLogger extends Logger {
public ErrorLogger(int level) {
this.level = level;
}
protected void write(String message) {
System.out.println("Error Console::Logger: " + message);
}
}
Mediator Pattern
The Mediator pattern defines an object that encapsulates how a set of objects interact. This pattern promotes loose coupling by preventing objects from referring to each other explicitly and allows their interaction to be varied independently.
Java Implementation:
interface Mediator {
void notify(Component sender, String event);
}
class ConcreteMediator implements Mediator {
private Button button;
private TextBox textBox;
public void setButton(Button button) {
this.button = button;
}
public void setTextBox(TextBox textBox) {
this.textBox = textBox;
}
public void notify(Component sender, String event) {
if (sender == button && event.equals("click")) {
textBox.setText("Button clicked");
}
}
}
class Component {
protected Mediator mediator;
public void setMediator(Mediator mediator) {
this.mediator = mediator;
}
}
class Button extends Component {
public void click() {
mediator.notify(this, "click");
}
}
class TextBox extends Component {
private String text;
public void setText(String text) {
this.text = text;
System.out.println("TextBox: " + text);
}
}
Best Practices for Implementing Design Patterns
Implementing design patterns effectively requires understanding their intent and applicability. Here are some tips:
- Understand the problem: Before selecting a pattern, ensure you understand the problem you’re trying to solve.
- Choose the right pattern: Not all patterns fit every problem. Select the pattern that best suits your needs.
- Keep it simple: Don’t overuse patterns. Overcomplicating your code with unnecessary patterns can make it harder to maintain.
- Refactor when necessary: Patterns can evolve over time as the system grows. Refactor your code to adapt to new requirements.
Design Patterns in Different Programming Languages
Different programming languages have different syntax and idioms, but the core concepts of design patterns remain the same. Here are some examples in popular languages:
Java
class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
Python
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(Singleton, cls).__new__(cls)
return cls._instance
C++
class Singleton {
private:
static Singleton* instance;
Singleton() {}
public:
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
};
Frequently Asked Questions about Design Patterns
What are design patterns?
Design patterns are typical solutions to common problems in software design. They are templates designed to help write code that is easy to understand and maintain.
Why should I use design patterns?
Design patterns help you write cleaner, more maintainable code. They promote code reusability and can improve the efficiency of your development process.
Are design patterns language-specific?
No, design patterns are not tied to any particular programming language. They can be implemented in any language, though the syntax will vary.
How many design patterns are there?
The “Gang of Four” book outlines 23 classic design patterns, but there are many more patterns that have been identified over time.
When should I use a Singleton pattern?
Use the Singleton pattern when you need to ensure that only one instance of a class is created and provide a global point of access to that instance.
What is the difference between Factory Method and Abstract Factory patterns?
The Factory Method pattern defines an interface for creating an object, but lets subclasses alter the type of objects that will be created. The Abstract Factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes.
Conclusion
Design patterns are essential tools for any software developer. They provide tested, proven development paradigms and promote code reusability and maintainability. Understanding and implementing design patterns can significantly improve your coding skills and the quality of your software projects. As you continue to develop your programming expertise, these patterns will become invaluable resources in your toolkit.