The Essential Design Patterns Every Developer Should Know
Design patterns are a crucial aspect of software engineering. They provide reusable solutions to common problems and promote best practices in software design. Understanding and applying these patterns can significantly improve code quality, readability, and maintainability. In this post, we’ll explore the most important design patterns, categorized into creational, structural, and behavioral patterns, with practical examples in Java.
Creational Design Patterns
Creational patterns deal with object creation mechanisms, aiming to create objects in a manner suitable for the situation.
Singleton Pattern
Purpose: Ensures a class has only one instance and provides a global point of access to it.
There are two forms of singleton design pattern
Early Instantiation: creation of instance at load time.
Lazy Instantiation: creation of instance when required.
Example:
1. Early Instantiation (Eager Initialization):
In this approach, the singleton instance is created as soon as the class is loaded by the JVM. This ensures the instance is always available but might be unnecessary overhead if the functionality is not used throughout the application.
Java
public class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() {
// Optional initialization logic
}
public static EagerSingleton getInstance() {
return instance;
}
}
2. Lazy Instantiation:
Here, the singleton instance is created only when it's first requested. This approach is more memory-efficient but requires handling potential thread-safety issues.
There are two common ways to implement lazy initialization:
a) Using a static initializer block:
public class LazySingleton {
private static LazySingleton instance;
static {
instance = new LazySingleton();
}
private LazySingleton() {
// Optional initialization logic
}
public static LazySingleton getInstance() {
return instance;
}
}
b) Using a thread-safe approach with a synchronized method:
public class ThreadSafeLazySingleton {
private static ThreadSafeLazySingleton instance;
private ThreadSafeLazySingleton() {
// Optional initialization logic
}
public static ThreadSafeLazySingleton getInstance() {
if (instance == null) {
synchronized(ThreadSafeLazySingleton.class) {
instance = new ThreadSafeLazySingleton();
}
}
return instance;
}
}
Use Cases:
Managing a connection to a database.
Logging instances where only one instance is needed.
Factory Method Pattern
Purpose: Defines an interface for creating an object but lets subclasses alter the type of objects that will be created.
Example:
abstract class Animal {
public abstract String makeSound();
}
class Dog extends Animal {
@Override
public String makeSound() {
return "Woof";
}
}
class Cat extends Animal {
@Override
public String makeSound() {
return "Meow";
}
}
class AnimalFactory {
public Animal createAnimal(String type) {
if (type.equals("Dog")) {
return new Dog();
} else if (type.equals("Cat")) {
return new Cat();
}
return null;
}
}
Use Cases:
- Creating objects without specifying the exact class of object that will be created.
Builder Pattern
Purpose: Separates the construction of a complex object from its representation so that the same construction process can create different representations.
Example:
public class House {
private int windows;
private int doors;
private House(HouseBuilder builder) {
this.windows = builder.windows;
this.doors = builder.doors;
}
public static class HouseBuilder {
private int windows;
private int doors;
public HouseBuilder setWindows(int windows) {
this.windows = windows;
return this;
}
public HouseBuilder setDoors(int doors) {
this.doors = doors;
return this;
}
public House build() {
return new House(this);
}
}
@Override
public String toString() {
return "House{" +
"windows=" + windows +
", doors=" + doors +
'}';
}
}
class Demo {
public static void main(String[] args) {
House house = new House.HouseBuilder()
.setDoors(2)
.setWindows(4)
.build();
System.out.println(house.toString());
}
}
Use Cases:
- Creating complex objects step-by-step with various configurations.
Structural Design Patterns
Structural patterns deal with object composition or the organization of classes and objects to form larger structures.
Adapter Pattern
Purpose: Allows incompatible interfaces to work together by wrapping an incompatible object.
Example:
public interface Printer {
void print();
}
public class LegacyPrinter {
public void printDocument() {
System.out.println("Legacy Printer is printing a document.");
}
}
public class PrinterAdapter implements Printer {
private LegacyPrinter legacyPrinter;
public PrinterAdapter(LegacyPrinter legacyPrinter) {
this.legacyPrinter = legacyPrinter;
}
@Override
public void print() {
legacyPrinter.printDocument();
}
}
// Client Code
public void clientCode(Printer printer) {
printer.print();
}
public class Main {
public static void main(String[] args) {
// Using the Adapter
PrinterAdapter adapter = new PrinterAdapter(new LegacyPrinter());
adapter.print();
}
}
Use Cases:
- Integrating with legacy systems or components that have incompatible interfaces.
Decorator Pattern
Purpose: Adds responsibilities to objects dynamically. Provides a flexible alternative to subclassing for extending functionality.
Example:
interface Shape {
void draw();
}
class Circle implements Shape {
public void draw() {
System.out.println("Drawing Circle");
}
}
class RedShapeDecorator implements Shape {
protected Shape decoratedShape;
public RedShapeDecorator(Shape decoratedShape) {
this.decoratedShape = decoratedShape;
}
public void draw() {
decoratedShape.draw();
setRedBorder(decoratedShape);
}
private void setRedBorder(Shape decoratedShape) {
System.out.println("Border Color: Red");
}
}
class Demo {
public static void main(String[] args) {
Shape circle = new Circle();
Shape redCircle = new RedShapeDecorator(new Circle());
System.out.println("Circle with normal border");
circle.draw();
System.out.println("\nCircle with red border");
redCircle.draw();
}
}
Use Cases:
- Dynamically adding new functionalities to objects without altering their structure.
Composite Pattern
Purpose: Composes objects into tree structures to represent part-whole hierarchies. Allows clients to treat individual objects and compositions uniformly.
Example:
import java.util.ArrayList;
import java.util.List;
interface Employee {
void showEmployeeDetails();
}
class Developer implements Employee {
private String name;
private long empId;
private String position;
public Developer(long empId, String name, String position) {
this.empId = empId;
this.name = name;
this.position = position;
}
public void showEmployeeDetails() {
System.out.println(empId + " " + name);
}
}
class Manager implements Employee {
private String name;
private long empId;
private String position;
public Manager(long empId, String name, String position) {
this.empId = empId;
this.name = name;
this.position = position;
}
public void showEmployeeDetails() {
System.out.println(empId + " " + name);
}
}
class CompanyDirectory implements Employee {
private List<Employee> employeeList = new ArrayList<Employee>();
public void showEmployeeDetails() {
for(Employee emp: employeeList) {
emp.showEmployeeDetails();
}
}
public void addEmployee(Employee emp) {
employeeList.add(emp);
}
public void removeEmployee(Employee emp) {
employeeList.remove(emp);
}
}
class Demo {
public static void main(String[] args) {
Developer dev1 = new Developer(100, "John Doe", "Pro Developer");
Developer dev2 = new Developer(101, "Jane Doe", "Developer");
Manager manager1 = new Manager(200, "Mary Smith", "Manager");
CompanyDirectory directory = new CompanyDirectory();
directory.addEmployee(dev1);
directory.addEmployee(dev2);
directory.addEmployee(manager1);
directory.showEmployeeDetails();
}
}
Use Cases:
- Hierarchical tree structures such as file systems or organizational structures.
Behavioral Design Patterns
Behavioral patterns focus on the interactions and responsibilities between objects.
Observer Pattern
Purpose: Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
Example:
import java.util.ArrayList;
import java.util.List;
interface Observer {
void update(String message);
}
class Subject {
private List<Observer> observers = new ArrayList<>();
public void attach(Observer observer) {
observers.add(observer);
}
public void detach(Observer observer) {
observers.remove(observer);
}
public void notifyObservers(String message) {
for (Observer observer : observers) {
observer.update(message);
}
}
}
class ConcreteObserver implements Observer {
private String name;
public ConcreteObserver(String name) {
this.name = name;
}
public void update(String message) {
System.out.println(name + " received message: " + message);
}
}
class Demo {
public static void main(String[] args) {
Subject subject = new Subject();
Observer observer1 = new ConcreteObserver("Observer 1");
Observer observer2 = new ConcreteObserver("Observer 2");
subject.attach(observer1);
subject.attach(observer2);
subject.notifyObservers("Hello, Observers!");
}
}
Use Cases:
- Implementing event handling systems or notifications.
trategy Pattern
Purpose: Defines a family of algorithms, encapsulates each one, and makes them interchangeable. Lets the algorithm vary independently from the clients that use it.
Example:
interface Strategy {
int doOperation(int num1, int num2);
}
class AddOperation implements Strategy {
public int doOperation(int num1, int num2) {
return num1 + num2;
}
}
class SubtractOperation implements Strategy {
public int doOperation(int num1, int num2) {
return num1 - num2;
}
}
class Context {
private Strategy strategy;
public Context(Strategy strategy) {
this.strategy = strategy;
}
public int executeStrategy(int num1, int num2) {
return strategy.doOperation(num1, num2);
}
}
class Demo {
public static void main(String[] args) {
Context context = new Context(new AddOperation());
System.out.println("10 + 5 = " + context.executeStrategy(10, 5));
context = new Context(new SubtractOperation());
System.out.println("10 - 5 = " + context.executeStrategy(10, 5));
}
}
Use Cases:
- Implementing multiple algorithms that can be chosen at runtime.
Command Pattern
Purpose: Encapsulates a request as an object, thereby allowing parameterization of clients with different requests, queuing of requests, and logging the requests.
Example:
interface Command {
void execute();
}
class Light {
public void turnOn() {
System.out.println("The light is on");
}
public void turnOff() {
System.out.println("The light is off");
}
}
class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.turnOn();
}
}
class LightOffCommand implements Command {
private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
public void execute() {
light.turnOff();
}
}
class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
}
}
class Demo {
public static void main(String[] args) {
Light light = new Light();
Command lightOn = new LightOnCommand(light);
Command lightOff = new LightOffCommand(light);
RemoteControl remote = new RemoteControl();
remote.setCommand(lightOn);
remote.pressButton();
remote.setCommand(lightOff);
remote.pressButton();
}
}
Use Cases:
- Implementing undoable operations or transaction logs.