Structural Design Patterns
Once you have objects, the next question is how to connect them: an old class that doesn't fit your new interface, a feature you want to add without touching existing code, a complicated system you want to hide behind one simple door. Structural patterns are seven proven ways of fitting classes and objects together. Most of them work by wrapping one object inside another, and the difference lies in why you wrap.
The Seven at a Glance
| Pattern | The problem | The idea |
|---|---|---|
| Adapter | A class has the wrong shape for the code that wants to use it | Put a converter in between |
| Bridge | Two things vary independently, and subclasses would multiply | Keep them in two separate class families, joined by a field |
| Composite | Groups and single items should be handled the same way | Make the group implement the same interface as the item |
| Decorator | You want to add features without changing a class | Wrap the object in another that adds the feature |
| Facade | A system has too many moving parts for callers | Offer one simple front door |
| Flyweight | Thousands of objects repeat the same data | Share one object for the repeated part |
| Proxy | Access to an object must be controlled or delayed | Put a stand-in in front of it |
Adapter
A phone charger from India won't fit a socket in the UK, so you carry a plug adapter. In code, an adapter lets a class with the "wrong" methods be used where a different interface is expected, without changing either side.
public class AdapterDemo {
interface PaymentMethod { String pay(long rupees); }
static class OldBankApi {
String makeTransfer(double amountInPaise) {
return "Transferred " + (long) amountInPaise + " paise";
}
}
static class BankAdapter implements PaymentMethod {
private final OldBankApi bank = new OldBankApi();
public String pay(long rupees) {
return bank.makeTransfer(rupees * 100.0);
}
}
public static void main(String[] args) {
PaymentMethod method = new BankAdapter();
System.out.println(method.pay(250));
}
}Transferred 25000 paiseOldBankApi speaks in paise through makeTransfer; the rest of the app speaks in rupees through pay. BankAdapter translates between them, and neither side had to change. Java's Arrays.asList(array) is an adapter that lets an array be used as a List.
Bridge
A TV remote and the TV are separate things. Remotes can be basic or advanced; devices can be TVs or radios. If you made a class for every pair (BasicTvRemote, AdvancedRadioRemote, and so on), the count would multiply with every new device. Bridge keeps remotes and devices in two separate families and connects them with a field.
public class BridgeDemo {
interface Device {
String name();
int volume();
void setVolume(int v);
}
static class Tv implements Device {
private int volume = 30;
public String name() { return "TV"; }
public int volume() { return volume; }
public void setVolume(int v) { volume = v; }
}
static class Radio implements Device {
private int volume = 50;
public String name() { return "Radio"; }
public int volume() { return volume; }
public void setVolume(int v) { volume = v; }
}
static class Remote {
protected final Device device;
Remote(Device device) { this.device = device; }
void volumeUp() { device.setVolume(device.volume() + 10); }
String status() { return device.name() + " volume " + device.volume(); }
}
static class AdvancedRemote extends Remote {
AdvancedRemote(Device device) { super(device); }
void mute() { device.setVolume(0); }
}
public static void main(String[] args) {
Remote basic = new Remote(new Tv());
basic.volumeUp();
System.out.println(basic.status());
AdvancedRemote advanced = new AdvancedRemote(new Radio());
advanced.volumeUp();
advanced.mute();
System.out.println(advanced.status());
}
}TV volume 40
Radio volume 0Any remote works with any device. A new device is one class, and a new kind of remote is one class; nothing multiplies. JDBC works like this: one Java database API, with a separate driver for each database.
Composite
A folder can contain files, and also other folders. Yet when you ask "how big is it?", you want the same question to work for a single file and for a whole folder. Composite makes the group implement the same interface as a single item, so code can treat them alike.
import java.util.ArrayList;
import java.util.List;
public class CompositeDemo {
interface Item { long size(); }
record FileItem(String name, long size) implements Item { }
static class Folder implements Item {
private final List<Item> items = new ArrayList<>();
Folder add(Item item) {
items.add(item);
return this;
}
public long size() {
long total = 0;
for (Item i : items) {
total += i.size();
}
return total;
}
}
public static void main(String[] args) {
Folder photos = new Folder()
.add(new FileItem("trip.jpg", 300))
.add(new FileItem("cake.jpg", 200));
Folder home = new Folder()
.add(new FileItem("notes.txt", 5))
.add(photos);
System.out.println("Photos: " + photos.size() + " KB");
System.out.println("Home: " + home.size() + " KB");
}
}Photos: 500 KB
Home: 505 KBFolder.size() simply asks each item for its size, whether that item is a file or another folder. The nesting can go as deep as you like.
Decorator
You order a filter coffee, then ask for milk, then for it to be sugar-free. It's still the same coffee, with extras wrapped around it, and each extra adds to the price. A decorator wraps an object in another object with the same interface that adds one feature.
public class DecoratorDemo {
interface Coffee {
long cost();
String description();
}
static class FilterCoffee implements Coffee {
public long cost() { return 40; }
public String description() { return "Filter coffee"; }
}
static class WithMilk implements Coffee {
private final Coffee inner;
WithMilk(Coffee inner) { this.inner = inner; }
public long cost() { return inner.cost() + 15; }
public String description() { return inner.description() + ", milk"; }
}
static class SugarFree implements Coffee {
private final Coffee inner;
SugarFree(Coffee inner) { this.inner = inner; }
public long cost() { return inner.cost() + 10; }
public String description() {
return inner.description() + ", sugar-free";
}
}
public static void main(String[] args) {
Coffee order = new SugarFree(new WithMilk(new FilterCoffee()));
System.out.println(order.description() + ": Rs. " + order.cost());
}
}Filter coffee, milk, sugar-free: Rs. 65Each wrapper calls the coffee inside it and adds its own part. You can combine extras in any order without a class for every combination. Java's I/O classes are built this way: new BufferedReader(new FileReader("notes.txt")) wraps a file reader to add buffering.
Facade
In a hotel, you don't walk to housekeeping, the kitchen and the billing office yourself; you call reception, and reception deals with them. A facade is that front desk for code: one simple method that coordinates several complicated parts.
public class FacadeDemo {
static class Inventory {
boolean reserve(String item) {
System.out.println("Reserved " + item);
return true;
}
}
static class Payments {
void charge(String customer, long amount) {
System.out.println("Charged " + customer + " Rs. " + amount);
}
}
static class Shipping {
void ship(String item, String customer) {
System.out.println("Shipping " + item + " to " + customer);
}
}
static class ShopFacade {
private final Inventory inventory = new Inventory();
private final Payments payments = new Payments();
private final Shipping shipping = new Shipping();
void buy(String customer, String item, long price) {
if (inventory.reserve(item)) {
payments.charge(customer, price);
shipping.ship(item, customer);
}
}
}
public static void main(String[] args) {
new ShopFacade().buy("Srihitha", "Headphones", 2500);
}
}Reserved Headphones
Charged Srihitha Rs. 2500
Shipping Headphones to SrihithaThe caller makes one call, buy, and never needs to know the right order of the three steps. The parts still exist and can be used directly when needed; the facade just makes the common case easy.
Flyweight
A cinema has 500 seats, but only two kinds: Premium and Standard. Storing "Premium, Rs. 350" separately in 100 seat objects wastes memory. Flyweight keeps one shared object per kind, and every seat points to it.
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class FlyweightDemo {
record SeatType(String name, long price) { }
record Seat(int row, int number, SeatType type) { }
static class SeatTypes {
private static final Map<String, SeatType> cache = new HashMap<>();
static int created = 0;
static SeatType get(String name, long price) {
SeatType type = cache.get(name);
if (type == null) {
type = new SeatType(name, price);
cache.put(name, type);
created++;
}
return type;
}
}
public static void main(String[] args) {
List<Seat> seats = new ArrayList<>();
for (int row = 1; row <= 20; row++) {
for (int n = 1; n <= 25; n++) {
SeatType type = row <= 4
? SeatTypes.get("Premium", 350)
: SeatTypes.get("Standard", 200);
seats.add(new Seat(row, n, type));
}
}
System.out.println(seats.size() + " seats share "
+ SeatTypes.created + " SeatType objects");
System.out.println(seats.get(0).type() == seats.get(10).type());
}
}500 seats share 2 SeatType objects
trueEach seat still knows its own row and number, but the repeated part (name and price) is one shared object. You've met this idea already: the Integer cache and the string pool share objects the same way (see "Integer Cache in Java").
Proxy
You can't walk straight into a manager's cabin; the receptionist checks who you are first. A proxy stands in front of a real object, with the same interface, and decides whether and when to pass the request on. It's often used to check permissions or to delay creating something expensive until it's really needed.
public class ProxyDemo {
interface Report { String open(); }
static class SalaryReport implements Report {
SalaryReport() {
System.out.println("Loading salary report from disk");
}
public String open() { return "Salary data"; }
}
static class ReportProxy implements Report {
private final String user;
private SalaryReport real;
ReportProxy(String user) { this.user = user; }
public String open() {
if (!user.equals("Navika")) {
return "Access denied for " + user;
}
if (real == null) {
real = new SalaryReport();
}
return real.open();
}
}
public static void main(String[] args) {
System.out.println(new ReportProxy("Advitha").open());
Report report = new ReportProxy("Navika");
System.out.println(report.open());
System.out.println(report.open());
}
}Access denied for Advitha
Loading salary report from disk
Salary data
Salary dataHow it works
- Advitha's request is refused before the real report is even created.
- For Navika, the proxy creates the real report only on the first call (lazy loading), and reuses it after that: "Loading" prints once.
- Callers only see the
Reportinterface, so they can't tell a proxy from the real thing.
Common Mistakes
Confusing Decorator and Proxy. A decorator adds behavior the caller wants; a proxy controls access to the same behavior.
A facade that grows into a god class. It should coordinate, not contain all the logic.
Flyweights that can be changed. Shared objects must be immutable, or one seat's change would affect every seat.
Interview Corner
What are the structural design patterns?
They deal with composing classes and objects into larger structures. Adapter converts one interface into another. Bridge separates an abstraction from its implementation so both can vary. Composite treats groups and single objects uniformly. Decorator adds responsibilities by wrapping. Facade provides a simple interface to a complex subsystem. Flyweight shares common state among many objects to save memory. Proxy controls access to another object.
What is the difference between Adapter, Decorator and Proxy?
All three wrap an object. An Adapter changes its interface so it fits code expecting a different one. A Decorator keeps the same interface and adds new behavior. A Proxy keeps the same interface and controls access, for example by checking permissions, loading lazily or calling a remote object.
Practice
- Easy: Add a
WithChocolatedecorator (Rs. 20) toDecoratorDemo.javaand order a coffee with milk and chocolate. - Harder: Add a third device,
Speaker, toBridgeDemo.java, and use both remotes with it. How many new classes did you need?
Key Takeaways
Was this tutorial helpful?