Menu

Design Patterns with Java OOP

Design Patterns with Java OOP

Design patterns are reusable, battle-tested solutions to recurring design problems, not specific code you copy, but a proven shape of solution you adapt to your situation. This chapter covers six of the most widely used patterns, two from each of the three classic families: creational, structural, and behavioural.

Creational Patterns: Singleton and Factory

Creational patterns deal with how objects get created. The Singleton pattern ensures a class has exactly one instance for the entire application, with a single, controlled access point to it, commonly used for things like configuration managers or connection pools.

class AppConfig {

private static AppConfig instance;

private String setting;

private AppConfig() {     // private constructor — no one outside can call 'new'

setting = "default";

}

public static AppConfig getInstance() {

if (instance == null) {

instance = new AppConfig();

}

return instance;

}

}

AppConfig config1 = AppConfig.getInstance();

AppConfig config2 = AppConfig.getInstance();

System.out.println(config1 == config2);   // true — both refer to the exact same object

The Factory pattern moves object creation behind a method, so calling code asks for what it needs by intent, without knowing or caring exactly which concrete class gets built underneath.

interface Notification {

void send(String message);

}

class EmailNotification implements Notification {

public void send(String message) { System.out.println("Email: " + message); }

}

class SmsNotification implements Notification {

public void send(String message) { System.out.println("SMS: " + message); }

}

class NotificationFactory {

static Notification create(String type) {

return switch (type) {

case "email" -> new EmailNotification();

case "sms" -> new SmsNotification();

default -> throw new IllegalArgumentException("Unknown type: " + type);

};

}

}

Notification notification = NotificationFactory.create("email");

notification.send("Your order has shipped");

Structural Patterns: Decorator and Adapter

Structural patterns deal with how classes and objects are composed into larger structures. The Decorator pattern attaches new behaviour to an individual object dynamically, by wrapping it, without altering the original class or affecting other instances of it.

interface Coffee {

double cost();

}

class SimpleCoffee implements Coffee {

public double cost() { return 50; }

}

abstract class CoffeeDecorator implements Coffee {

protected Coffee wrapped;

CoffeeDecorator(Coffee c) { wrapped = c; }

}

class MilkDecorator extends CoffeeDecorator {

MilkDecorator(Coffee c) { super(c); }

public double cost() { return wrapped.cost() + 10; }

}

class SugarDecorator extends CoffeeDecorator {

SugarDecorator(Coffee c) { super(c); }

public double cost() { return wrapped.cost() + 5; }

}

Coffee order = new SugarDecorator(new MilkDecorator(new SimpleCoffee()));

System.out.println(order.cost());   // 65 — layered, without ever touching SimpleCoffee

The Adapter pattern lets two otherwise incompatible interfaces work together, by wrapping one of them in a class that translates its API into the shape the other side expects.

// An existing, third-party class with an interface you can't change

class LegacyPrinter {

void printOldFormat(String text) {

System.out.println("[LEGACY] " + text);

}

}

// The interface your new code actually expects

interface ModernPrinter {

void print(String text);

}

// Adapter bridges the gap between the two

class PrinterAdapter implements ModernPrinter {

private LegacyPrinter legacyPrinter;

PrinterAdapter(LegacyPrinter p) { legacyPrinter = p; }

public void print(String text) {

legacyPrinter.printOldFormat(text);   // translates the call underneath

}

}

ModernPrinter printer = new PrinterAdapter(new LegacyPrinter());

printer.print("Hello");   // [LEGACY] Hello

5.3 Behavioral Patterns: Observer and Strategy

Behavioural patterns deal with how objects communicate and assign responsibility among themselves. The Observer pattern defines a one-to-many relationship, where one object (the subject) notifies a list of dependent objects (observers) automatically whenever its state changes.

interface Observer {

void update(String event);

}

class EmailObserver implements Observer {

public void update(String event) {

System.out.println("Email alert: " + event);

}

}

class StockTicker {

private List<Observer> observers = new ArrayList<>();

void subscribe(Observer o) {

observers.add(o);

}

void priceChanged(String stock, double price) {

String event = stock + " is now $" + price;

for (Observer o : observers) {

o.update(event);   // every subscriber is notified automatically

}

}

}

StockTicker ticker = new StockTicker();

ticker.subscribe(new EmailObserver());

ticker.priceChanged("AAPL", 195.50);   // Email alert: AAPL is now $195.5

The Strategy pattern defines a family of interchangeable algorithms, each wrapped in its own class implementing a common interface, letting client code swap between them freely at runtime.

interface DiscountStrategy {

double apply(double price);

}

class NoDiscount implements DiscountStrategy {

public double apply(double price) { return price; }

}

class PercentageDiscount implements DiscountStrategy {

private double percent;

PercentageDiscount(double p) { percent = p; }

public double apply(double price) { return price - (price * percent / 100); }

}

class Checkout {

private DiscountStrategy strategy;

Checkout(DiscountStrategy s) { strategy = s; }

double finalPrice(double price) {

return strategy.apply(price);   // the algorithm is swapped in, not hardcoded

}

}

Checkout regular = new Checkout(new NoDiscount());

Checkout sale = new Checkout(new PercentageDiscount(20));

System.out.println(sale.finalPrice(1000));   // 800.0

Pattern

Family

Core Idea

Singleton

Creational

Exactly one instance, with one controlled global access point

Factory

Creational

Centralise object creation behind a method that returns by intent, not by concrete type

Decorator

Structural

Attach new behaviour to an object dynamically by wrapping it

Adapter

Structural

Translate one interface into another so incompatible code can work together

Observer

Behavioral

Notify many dependent objects automatically when one subject's state changes

Strategy

Behavioral

Make an algorithm swappable by encapsulating each variant behind a common interface

Did You Know?

Notice that the Strategy pattern and the Dependency Inversion Principle from Chapter 4 are really the same underlying idea, applied at different scales: depend on an interface, supply the concrete implementation from outside. Many design patterns are, at their core, simply disciplined, named applications of the SOLID principles to a specific, recurring shape of problem.