Traian-Florin Șerbănuță
2025
Reusable solutions to common software design problems.
Popularized by the “Gang of Four” (Gamma, Helm, Johnson, Vlissides, 1994).
Instead of reinventing how to traverse a collection, we apply the Iterator pattern.
Design patterns are typically grouped into three main categories:
| Category | Description | Example Patterns |
|---|---|---|
| Creational | How objects are created | Builder, Factory, Singleton |
| Structural | How classes and objects are composed | Adapter, Bridge, Composite, Decorator, Proxy |
| Behavioral | How objects interact and communicate | Visitor, Mediator, State |
Behavioral pattern
Objects you want to operate on (e.g., nodes in an AST, shapes in a graphics editor).
An object that implements different operations for each concrete element type.
visitCircle(circle),
visitSquare(square),
visitTriangle(triangle)
A crucial mechanism:
The element calls visitor.visitXYZ(this)
The visitor chooses the correct method based on the element type
This avoids type-checking or if/instanceof cascades.
. . .
The Visitor pattern lets you add new operations by creating new Visitor classes, while the shapes themselves remain unchanged and simply “accept” visitors.
// Element
interface Shape {
void accept(Visitor v);
}
// Concrete Elements
class Circle implements Shape {
double radius = 5;
public void accept(Visitor v) { v.visit(this); }
}
class Rectangle implements Shape {
double width = 4, height = 3;
public void accept(Visitor v) { v.visit(this); }
}
// Visitor
interface Visitor {
void visit(Circle c);
void visit(Rectangle r);
}
// Concrete Visitor
class AreaCalculator implements Visitor {
public void visit(Circle c) {
System.out.println("Circle area = " + Math.PI * c.radius * c.radius);
}
public void visit(Rectangle r) {
System.out.println("Rectangle area = " + r.width * r.height);
}
}
// Usage
public class Main {
public static void main(String[] args) {
Shape[] shapes = { new Circle(), new Rectangle() };
Visitor areaVisitor = new AreaCalculator();
for (Shape s : shapes) s.accept(areaVisitor);
}
}Behavioral pattern
How to reduce direct dependencies and complex communication between many interacting objects?
Mediator object that centralizes communication
logic.Defines how components communicate through the mediator.
Implements coordination logic.
Objects that interact only through the mediator.
In a chat application, every user needs to send messages to others.
If each user communicated directly with every other user, the system would become highly coupled and difficult to maintain.
. . .
The Mediator pattern introduces a central ChatRoom that manages all communication.
Users send messages through the mediator, drastically simplifying interaction.
// Mediator
interface ChatMediator {
void sendMessage(String msg, User user);
}
// Concrete Mediator
class ChatRoom implements ChatMediator {
public void sendMessage(String msg, User user) {
System.out.println(user.getName() + ": " + msg);
}
}
// Colleague
abstract class User {
protected ChatMediator mediator;
protected String name;
User(String name, ChatMediator mediator) {
this.name = name; this.mediator = mediator;
}
String getName() { return name; }
void send(String msg) { mediator.sendMessage(msg, this); }
}
// Concrete Colleague
class ChatUser extends User {
ChatUser(String name, ChatMediator mediator) { super(name, mediator); }
}
// Usage
public class Main {
public static void main(String[] args) {
ChatMediator room = new ChatRoom();
User alice = new ChatUser("Alice", room);
alice.send("Hello everyone!");
}
}Imagine a smart home system where devices (lights, thermostat, alarm, blinds) must coordinate actions (e.g., “away mode”).
Design a Mediator that centralizes communication so devices do not directly reference or call each other.
Outline the mediator role and how devices interact with it.
Structural pattern
How to avoid a class explosion caused by combining multiple abstractions with multiple implementations?
Defines high-level control logic.
Maintains a reference to an implementation.
Specialized abstractions that extend the base abstraction.
Defines low-level platform-specific operations.
Actual implementation details.
You want to build a universal remote-control system that works with different devices like TVs, Radios, and projectors.
If you directly subclass for every combination (e.g., AdvancedTVRemote, BasicRadioRemote), you get class explosion.
. . .
// Implementor
interface Device {
void turnOn();
void turnOff();
}
// Concrete Implementors
class TV implements Device {
public void turnOn() { System.out.println("TV ON"); }
public void turnOff() { System.out.println("TV OFF"); }
}
// Abstraction
abstract class Remote {
protected Device device;
Remote(Device d) { this.device = d; }
abstract void toggle();
}
// Refined Abstraction
class SimpleRemote extends Remote {
private boolean on = false;
SimpleRemote(Device d) { super(d); }
void toggle() {
if (on) device.turnOff();
else device.turnOn();
on = !on;
}
}
// Usage
public class Main {
public static void main(String[] args) {
Remote remote = new SimpleRemote(new TV());
remote.toggle();
remote.toggle();
}
}Add new abstractions or new implementations
without touching the other side
Architecture more complex than necessary for simple cases.
Adds layers of indirection you may not always need.
You are building a drawing tool with two dimensions of variability:
Explain how to apply the Bridge pattern so all shapes can be rendered with any rendering method without class explosion.
Identify separate dimensions of change and design a usable abstraction/ implementation split.
Structural pattern
Convert the interface of one class into another interface clients expect.
How to make incompatible interfaces work together without changing existing code?
Create an Adapter that wraps an existing class and
exposes the desired target interface.
The interface your code expects and uses.
The existing class with an incompatible interface.
The wrapper that Implements the Target interface
Internally calls the Adaptee method(s), translating data or behavior
play()
method,
playMp3().. . .
The Adapter pattern wraps the incompatible class and exposes the interface the client expects, allowing the two systems to work together seamlessly.
// Target interface
interface MediaPlayer {
void play(String file);
}
// Adaptee
class LegacyPlayer {
void playMp3(String filename) {
System.out.println("Playing MP3: " + filename);
}
}
// Adapter
class MediaAdapter implements MediaPlayer {
private LegacyPlayer legacy = new LegacyPlayer();
public void play(String file) { legacy.playMp3(file); }
}
// Usage
public class Main {
public static void main(String[] args) {
MediaPlayer player = new MediaAdapter();
player.play("song.mp3");
}
}Reuses existing code without modification
Decouples client code from concrete implementations
Makes third-party, legacy, or low-level APIs easier to use
Improves testability by exposing a clean interface
Adds an extra layer of indirection
Can proliferate adapters if many mismatched types exist
If misused, may hide architectural inconsistencies
A new external weather service provides data in a completely different format from your current WeatherData interface.
Design an Adapter that lets your system continue using WeatherData
Structural pattern
Attach additional responsibilities to an object dynamically without modifying its class.
How to add flexible, combinable features to objects without subclass explosion?
Wrap objects with decorator classes that implement the same interface and add behavior before/after delegating calls.
Defines the main operations.
The core object you want to decorate.
Wraps a component and delegates calls to it.
Add additional behavior before or after delegating to the wrapped object.
. . .
// Component
interface Beverage {
String getDescription();
double cost();
}
// Concrete Component
class Coffee implements Beverage {
public String getDescription() { return "Coffee"; }
public double cost() { return 2.0; }
}
// Decorator
abstract class AddOn implements Beverage {
protected Beverage beverage;
AddOn(Beverage b) { beverage = b; }
}
// Concrete Decorators
class Milk extends AddOn {
Milk(Beverage b) { super(b); }
public String getDescription() { return beverage.getDescription() + ", Milk"; }
public double cost() { return beverage.cost() + 0.5; }
}
// Usage
public class Main {
public static void main(String[] args) {
Beverage coffee = new Milk(new Coffee());
System.out.println(coffee.getDescription() + " = $" + coffee.cost());
}
}Add responsibilities at runtime
Combine decorators in flexible ways
Open/Closed Principle
Avoids deep subclass hierarchies
Lots of small classes
Behavior can become harder to trace after many layers
Debugging can be trickier
Structural pattern
Provide a surrogate or placeholder for another object to control access to it.
How to manage access to a resource-heavy or remote object (e.g., lazy loading, caching, security)?
Implement a proxy that implements the same interface as the real subject and controls access before forwarding requests.
Defines the operations available to both Proxy and RealSubject.
The actual object that does the real work.
Implements the same interface but performs extra steps before/after delegating to RealSubject: Access control, Lazy initialization, Logging / auditing, Remote communication, Caching
Accessing a real database connection is slow and expensive.
However, you only need the actual connection when a query is executed.
. . .
// Subject
interface Database {
void query(String sql);
}
// Real Subject
class RealDatabase implements Database {
public RealDatabase() {
System.out.println("Connecting to database...");
}
public void query(String sql) {
System.out.println("Executing query: " + sql);
}
}
// Proxy
class DatabaseProxy implements Database {
private RealDatabase db;
public void query(String sql) {
if (db == null) db = new RealDatabase(); // lazy initialization
db.query(sql);
}
}
// Usage
public class Main {
public static void main(String[] args) {
Database db = new DatabaseProxy();
db.query("SELECT * FROM users");
}
}Add functionality without changing the real object
Control expensive or sensitive operations
Transparent to clients—same interface
Can optimize performance (caching, lazy loading)
Supports distributed systems (remote proxy)
Adds complexity
Indirection may hurt performance if misused
Can hide what’s really happening (e.g., network calls look local)
Your application accesses remote image files stored on a cloud server.
Design a Proxy that loads the actual image only when it is displayed
(for example, when scrolling in a gallery).
Describe the responsibilities of both the proxy and the real image.
Identify opportunities for lazy loading, access control, and indirection.
Structural pattern
Compose objects into tree structures to represent part–whole hierarchies.
How to treat individual objects and groups of objects uniformly?
Define a common component interface
Defines operations available to both leaf and composite objects.
A simple object with no children.
A container object that can hold components (both leaves and other composites).
Interacts with components uniformly, without caring if they’re leaves or composites.
You want to represent a hierarchical file system where folders can contain both files and other folders.
Clients should treat individual files and folder groups uniformly
(e.g., calling show() on either should work).
. . .
// Component
interface FileSystemNode {
void show();
}
// Leaf
class FileNode implements FileSystemNode {
private String name;
FileNode(String name) { this.name = name; }
public void show() { System.out.println("File: " + name); }
}
// Composite
class Folder implements FileSystemNode {
private String name;
private java.util.List<FileSystemNode> children = new java.util.ArrayList<>();
Folder(String name) { this.name = name; }
public void add(FileSystemNode node) { children.add(node); }
public void show() {
System.out.println("Folder: " + name);
for (FileSystemNode child : children) child.show();
}
}
// Usage
public class Main {
public static void main(String[] args) {
Folder root = new Folder("root");
root.add(new FileNode("file1.txt"));
Folder sub = new Folder("subfolder");
sub.add(new FileNode("file2.txt"));
root.add(sub);
root.show();
}
}| # Composite Pattern — benefits and drawbacks |
| ## Benefits |
| - Treat leaves and composites the same |
| - Simplifies client code |
| - Makes tree structures easy to build and manipulate |
| - Supports recursive operations elegantly |
| - Encourages flexible, extensible system architecture |
| ## Drawbacks |
| - Can make type distinctions harder when you do need to handle leaf/composite differently |
| - Risk of making Composite overly general |
| - May expose child-management methods even for leaves (depending on design) |
You are modeling hierarchical UI components:
Explain how the Composite pattern allows you to treat every UI element uniformly (e.g., calling render() or resize()).
Sketch the component interface and the composite structure.
Add operations to existing class hierarchies without modifying them by externalizing behavior.
Reduce tangled, many-to-many communication by centralizing interaction logic inside a mediator object.
Separate abstraction from implementation; avoid class explosion and allow sides to vary independently.
Make incompatible interfaces work together: wrap one interface to match expectations of another.
Dynamically add responsibilities or behavior to objects without subclassing or modifying original classes.
Control/enhance access to an object (lazy loading, security, caching) without changing the real object.
Treat individual items and groups uniformly through a shared component interface.