Skip to content
Dev Dump

Structural Patterns

Structural patterns explain how to assemble objects and classes into larger structures, while keeping the structures flexible and efficient.

The Adapter Pattern is a structural design pattern that allows objects with incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces, making them compatible without changing their existing code.

Adapter Pattern

Adapter Pattern

Adapter Pattern

public interface Socket {
void charge();
}
public class BritishSocket {
public void plugIn() {
System.out.println("Plugged into British Socket");
}
}
public class EuropeanSocket {
public void connect() {
System.out.println("Connected to European Socket");
}
}
public class SocketAdapter implements Socket {
private BritishSocket britishSocket;
private EuropeanSocket europeanSocket;
public SocketAdapter(BritishSocket britishSocket) {
this.britishSocket = britishSocket;
}
public SocketAdapter(EuropeanSocket europeanSocket) {
this.europeanSocket = europeanSocket;
}
@Override
public void charge() {
if (britishSocket != null) {
britishSocket.plugIn();
} else if (europeanSocket != null) {
europeanSocket.connect();
}
}
}
public class AdapterPatternExample {
public static void main(String[] args) {
BritishSocket britishSocket = new BritishSocket();
EuropeanSocket europeanSocket = new EuropeanSocket();
SocketAdapter adapter1 = new SocketAdapter(britishSocket);
SocketAdapter adapter2 = new SocketAdapter(europeanSocket);
// Charge using British socket adapter
adapter1.charge();
// Charge using European socket adapter
adapter2.charge();
}
}
  • Allows Reusability: Adapters can be reused for different Adaptees and Targets.
  • Enhances Flexibility: Allows the system to work with new classes without modifying their code.
  • Promotes Decoupling: Separates the client code from the complexities of interfacing with different systems.

Composite literally means made up of various elements or parts

  • Intent: Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.
  • Use When: You want to represent part-whole hierarchies of objects. You want clients to be able to ignore the difference between compositions of objects and individual objects.

Composite Pattern

import java.util.ArrayList;
import java.util.List;
// Component Interface
interface Component {
void displayPrice();
}
// Leaf
class Leaf implements Component {
private String name;
private int price;
public Leaf(String name, int price) {
this.name = name;
this.price = price;
}
@Override
public void displayPrice() {
System.out.println(name + " : " + price);
}
}
// Composite
class Composite implements Component {
private String name;
private List<Component> components = new ArrayList<>();
public Composite(String name) {
this.name = name;
}
public void addComponent(Component component) {
components.add(component);
}
@Override
public void displayPrice() {
System.out.println(name + ":");
for (Component component : components) {
component.displayPrice();
}
}
}
// Usage:
Leaf mouse = new Leaf("Mouse", 50);
Leaf keyboard = new Leaf("Keyboard", 100);
Leaf monitor = new Leaf("Monitor", 300);
Leaf ram = new Leaf("RAM", 150);
Leaf hdd = new Leaf("HDD", 200);
Composite cabinet = new Composite("Cabinet");
cabinet.addComponent(ram);
cabinet.addComponent(hdd);
Composite computer = new Composite("Computer");
computer.addComponent(mouse);
computer.addComponent(keyboard);
computer.addComponent(monitor);
computer.addComponent(cabinet);
computer.displayPrice();
// Output:
// Computer:
// Mouse : 50
// Keyboard : 100
// Monitor : 300
// Cabinet:
// RAM : 150
// HDD : 200
  • Explanation: The Component interface defines the common operation (displayPrice). Leaf represents individual components. Composite represents a group of components. The displayPrice method in the Composite class iterates through all its children and calls their displayPrice methods, allowing you to treat individual objects and compositions uniformly.
  • Intent: Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
  • Use When: You want to add responsibilities to individual objects without affecting other objects. Extension by subclassing is impractical.

Decorator Pattern

// Component Interface
interface Coffee {
String getDescription();
double getCost();
}
// Concrete Component
class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Simple coffee";
}
@Override
public double getCost() {
return 2.0;
}
}
// Decorator
abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee decoratedCoffee) {
this.decoratedCoffee = decoratedCoffee;
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription();
}
@Override
public double getCost() {
return decoratedCoffee.getCost();
}
}
// Concrete Decorators
class Milk extends CoffeeDecorator {
public Milk(Coffee decoratedCoffee) {
super(decoratedCoffee);
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", with milk";
}
@Override
public double getCost() {
return decoratedCoffee.getCost() + 0.5;
}
}
class Sugar extends CoffeeDecorator {
public Sugar(Coffee decoratedCoffee) {
super(decoratedCoffee);
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", with sugar";
}
@Override
public double getCost() {
return decoratedCoffee.getCost() + 0.2;
}
}
// Usage:
Coffee coffee = new SimpleCoffee();
System.out.println(coffee.getDescription() + " Cost: " + coffee.getCost());
// Output: Simple coffee Cost: 2.0
coffee = new Milk(coffee);
System.out.println(coffee.getDescription() + " Cost: " + coffee.getCost());
// Output: Simple coffee, with milk Cost: 2.5
coffee = new Sugar(coffee);
System.out.println(coffee.getDescription() + " Cost: " + coffee.getCost());
// Output: Simple coffee, with milk, with sugar Cost: 2.7
  • Explanation: The Coffee interface is the component. SimpleCoffee is a concrete component. CoffeeDecorator is the abstract decorator. Milk and Sugar are concrete decorators that add functionality (milk and sugar) to the coffee object dynamically. You can chain decorators together to add multiple responsibilities.
  • Intent: Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.
  • Use When: You want to provide a simple interface to a complex subsystem. You want to reduce dependencies between clients and the subsystem.

Facade Pattern

// Subsystem Classes
class CPU {
public void freeze() {
System.out.println("CPU Freeze");
}
public void jump(long position) {
System.out.println("CPU Jump to position " + position);
}
public void execute() {
System.out.println("CPU Execute");
}
}
class Memory {
public void load(long position, byte[] data) {
System.out.println("Memory Load data to position " + position);
}
}
class HardDrive {
public byte[] read(long lba, int size) {
System.out.println("HardDrive Read from lba " + lba + " size " + size);
return new byte[size]; // Simulate data read
}
}
// Facade
class ComputerFacade {
private CPU cpu;
private Memory memory;
private HardDrive hardDrive;
public ComputerFacade() {
this.cpu = new CPU();
this.memory = new Memory();
this.hardDrive = new HardDrive();
}
public void startComputer() {
cpu.freeze();
memory.load(0, hardDrive.read(100, 1024));
cpu.jump(0);
cpu.execute();
}
}
// Usage:
ComputerFacade computer = new ComputerFacade();
computer.startComputer();
// Output:
// CPU Freeze
// HardDrive Read from lba 100 size 1024
// Memory Load data to position 0
// CPU Jump to position 0
// CPU Execute
  • Explanation: The CPUMemory, and HardDrive classes represent a complex subsystem. The ComputerFacade provides a simplified interface (startComputer()) to the client, hiding the complexity of the subsystem.

proxy is the authority to represent someone else.

Proxy Pattern

  • Intent: Provide a surrogate or placeholder for another object to control access to it.
  • Use When: You want to control access to an object. The object is expensive to create and you want to delay its creation. You want to add functionality (like security checks) when accessing an object.
// Subject Interface
interface Image {
void display();
}
// Real Subject
class RealImage implements Image {
private String fileName;
public RealImage(String fileName) {
this.fileName = fileName;
loadFromDisk(fileName);
}
private void loadFromDisk(String fileName) {
System.out.println("Loading " + fileName);
}
@Override
public void display() {
System.out.println("Displaying " + fileName);
}
}
// Proxy
class ProxyImage implements Image {
private String fileName;
private RealImage realImage;
public ProxyImage(String fileName) {
this.fileName = fileName;
}
@Override
public void display() {
if (realImage == null) {
realImage = new RealImage(fileName);
}
realImage.display();
}
}
// Usage:
Image image = new ProxyImage("test_image.jpg");
// Image will be loaded from disk only when display() is called
image.display();
// Output:
// Loading test_image.jpg
// Displaying test_image.jpg
// Image is already loaded, so it will be displayed directly
image.display();
// Output:
// Displaying test_image.jpg
  • Explanation: The Image interface is the subject. RealImage is the real subject that is expensive to create (loading from disk). ProxyImage acts as a proxy. It creates the RealImage object only when it’s needed, implementing lazy loading.

Definition: The Bridge Pattern is a structural design pattern that separates an abstraction from its implementation so that the two can vary independently.

Suppose we have:

  • 2 Shapes: Circle, Square

  • 2 Drawing APIs: OpenGL, DirectX

👎 Naive Implementation (Before Bridge Pattern)

Section titled “👎 Naive Implementation (Before Bridge Pattern)”

Without using the Bridge pattern, you might try to handle every combination with separate subclasses:

class OpenGLCircle {
public void draw() {
System.out.println("Drawing Circle using OpenGL");
}
}
class DirectXCircle {
public void draw() {
System.out.println("Drawing Circle using DirectX");
}
}
class OpenGLSquare {
public void draw() {
System.out.println("Drawing Square using OpenGL");
}
}
class DirectXSquare {
public void draw() {
System.out.println("Drawing Square using DirectX");
}
}
  • For every new shape, you need to create new classes for every drawing API.
  • If you add 1 more shape and 1 more API, the number of classes grows exponentially.
  • Tightly coupled logic: Drawing logic and shape logic are mixed together.
Shapes ↓ / APIs →OpenGLDirectX
Circle
Square
Triangle (new)
Vulkan (new API)

To support Triangle + Vulkan, you’d need 3 × 3 = 9 classes total!

Now let’s refactor this using the Bridge Pattern:

interface DrawingAPI {
void drawCircle(double x, double y, double radius);
void drawSquare(double x, double y, double size);
}
class OpenGLAPI implements DrawingAPI {
public void drawCircle(double x, double y, double radius) {
System.out.println("OpenGL: Drawing circle at (" + x + "," + y + ") with radius " + radius);
}
public void drawSquare(double x, double y, double size) {
System.out.println("OpenGL: Drawing square at (" + x + "," + y + ") with size " + size);
}
}
class DirectXAPI implements DrawingAPI {
public void drawCircle(double x, double y, double radius) {
System.out.println("DirectX: Drawing circle at (" + x + "," + y + ") with radius " + radius);
}
public void drawSquare(double x, double y, double size) {
System.out.println("DirectX: Drawing square at (" + x + "," + y + ") with size " + size);
}
}
abstract class Shape {
protected DrawingAPI drawingAPI;
protected Shape(DrawingAPI drawingAPI) {
this.drawingAPI = drawingAPI;
}
public abstract void draw();
}
class Circle extends Shape {
private double x, y, radius;
public Circle(double x, double y, double radius, DrawingAPI drawingAPI) {
super(drawingAPI);
this.x = x;
this.y = y;
this.radius = radius;
}
public void draw() {
drawingAPI.drawCircle(x, y, radius);
}
}
class Square extends Shape {
private double x, y, size;
public Square(double x, double y, double size, DrawingAPI drawingAPI) {
super(drawingAPI);
this.x = x;
this.y = y;
this.size = size;
}
public void draw() {
drawingAPI.drawSquare(x, y, size);
}
}
public class BridgePatternDemo {
public static void main(String[] args) {
Shape circle1 = new Circle(5, 10, 2, new OpenGLAPI());
Shape circle2 = new Circle(15, 30, 5, new DirectXAPI());
Shape square1 = new Square(0, 0, 3, new OpenGLAPI());
Shape square2 = new Square(5, 5, 6, new DirectXAPI());
circle1.draw();
circle2.draw();
square1.draw();
square2.draw();
}
}

The Flyweight Pattern is a structural design pattern used to minimize memory usage by sharing as much data as possible with similar objects.

It separates the intrinsic (shared) state from the extrinsic (unique) state, so that shared parts of objects are stored only once and reused wherever needed.

import java.util.*;
// ================ Tree Class =================
class Tree {
// Attributes that keep on changing
private int x;
private int y;
// Attributes that remain constant
private String name;
private String color;
private String texture;
public Tree(int x, int y, String name, String color, String texture) {
this.x = x;
this.y = y;
this.name = name;
this.color = color;
this.texture = texture;
}
public void draw() {
System.out.println("Drawing tree at (" + x + ", " + y + ") with type " + name);
}
}
// ================ Forest Class =================
class Forest {
private List<Tree> trees = new ArrayList<>();
public void plantTree(int x, int y, String name, String color, String texture) {
Tree tree = new Tree(x, y, name, color, texture);
trees.add(tree);
}
public void draw() {
for (Tree tree : trees) {
tree.draw();
}
}
}
// =============== Client Code ==================
class Main {
public static void main(String[] args) {
Forest forest = new Forest();
// Planting 1 million trees
for(int i = 0; i < 1000000; i++) {
forest.plantTree(i, i, "Oak", "Green", "Rough");
}
System.out.println("Planted 1 million trees.");
}
}

Although the above codes works absolutely fine but there are a few problems associated with it:

  • Redundant memory usage: Same tree data duplicated a million times.
  • Inefficient: Slower rendering, higher GC overhead.

The previous implementation created a new Tree object for each of the 1 million trees, even when most of them had identical properties like name, color, and texture. This led to unnecessary duplication of memory for the shared attributes.

import java.util.*;
// ============= TreeType Class ================
class TreeType {
// Properties that are common among all trees of this type
private String name;
private String color;
private String texture;
public TreeType(String name, String color, String texture) {
this.name = name;
this.color = color;
this.texture = texture;
}
public void draw(int x, int y) {
System.out.println("Drawing " + name + " tree at (" + x + ", " + y + ")");
}
}
// ================ Tree Class =================
class Tree {
// Attributes that keep on changing
private int x;
private int y;
// Attributes that remain constant
private TreeType treeType;
public Tree(int x, int y, TreeType treeType) {
this.x = x;
this.y = y;
this.treeType = treeType;
}
public void draw() {
treeType.draw(x, y);
}
}
// ============ TreeFactory Class ==============
class TreeFactory {
static Map<String, TreeType> treeTypeMap = new HashMap<>();
public static TreeType getTreeType(String name, String color, String texture) {
String key = name + " - " + color + " - " + texture;
if (!treeTypeMap.containsKey(key)) {
treeTypeMap.put(key, new TreeType(name, color, texture));
}
return treeTypeMap.get(key);
}
}
// ================ Forest Class =================
class Forest {
private List<Tree> trees = new ArrayList<>();
public void plantTree(int x, int y, String name, String color, String texture) {
Tree tree = new Tree(x, y, TreeFactory.getTreeType(name, color, texture));
trees.add(tree);
}
public void draw() {
for (Tree tree : trees) {
tree.draw();
}
}
}
// =============== Client Code ==================
class Main {
public static void main(String[] args) {
Forest forest = new Forest();
// Planting 1 million trees
for(int i = 0; i < 1000000; i++) {
forest.plantTree(i, i, "Oak", "Green", "Rough");
}
System.out.println("Planted 1 million trees.");
}
}
ScenarioUse Flyweight?
Huge number of similar objects (e.g. pixels, chars)✅ Yes
Objects share data (intrinsic state)✅ Yes
Memory usage is critical✅ Yes
Objects have minor variations✅ Yes