Structural Design Patterns #
Structural patterns concern the composition of classes and objects into larger structures. They help ensure that when parts of a system change, the overall structure remains flexible and efficient.
Adapter #
Motivation: We need to use an existing class whose interface is incompatible with what our code expects, and we cannot (or do not want to) modify that class (e.g., because it lives in a third-party library).
Intent: Convert the interface of a class into another interface that clients expect.

The adapter wraps the incompatible class behind a stable interface that the rest of the system depends on. This isolates third-party or legacy dependencies to a single boundary class. If the external library changes, only the adapter needs updating, not every caller.
interface MediaPlayer {
fun play(filename: String)
}
class VlcPlayer {
fun playVlc(filename: String) { /* ... */ }
}
class VlcAdapter(private val vlcPlayer: VlcPlayer) : MediaPlayer {
override fun play(filename: String) = vlcPlayer.playVlc(filename)
}
Composite #
Motivation: An application has recursive groupings of primitives and containers (e.g., files and directories, basic shapes and compound shapes), and client code should treat individual elements and compositions uniformly.
Intent: Compose objects into tree structures and let clients work with individual objects and compositions through a single interface.

Because both Leaf and Composite implement the same Component interface, client code doesn’t need to distinguish between a single element and a group of elements.
It simply calls operation() on any Component without knowing or caring whether that component is a leaf or contains a whole subtree.
interface Graphic {
fun draw()
}
class Circle : Graphic {
override fun draw() { /* draw circle */ }
}
class CompositeGraphic : Graphic {
private val children = mutableListOf<Graphic>()
fun add(g: Graphic) { children.add(g) }
fun remove(g: Graphic) { children.remove(g) }
override fun draw() { children.forEach { it.draw() } }
}
Decorator #
Motivation: We want to add responsibilities to an object dynamically at runtime, rather than statically through inheritance. Subclassing every combination of features would lead to a combinatorial explosion of classes.
Intent: Attach additional responsibilities to an object dynamically, providing a flexible alternative to subclassing.

A decorator wraps a component and forwards calls to it, adding behavior before or after. Because decorators and concrete components share the same interface, decorators can be stacked in any combination—each decorator class stays focused on a single added responsibility, and new combinations don’t require new classes.
interface Beverage {
fun cost(): Double
fun description(): String
}
class Coffee : Beverage {
override fun cost() = 1.5
override fun description() = "Coffee"
}
class Tea : Beverage {
override fun cost() = 1.0
override fun description() = "Tea"
}
abstract class BeverageDecorator(private val beverage: Beverage) : Beverage {
abstract override fun cost(): Double
abstract override fun description(): String
}
class MilkDecorator(beverage: Beverage) : BeverageDecorator(beverage) {
override fun cost() = super.cost() + 0.5
override fun description() = super.description() + " + milk"
}
class SugarDecorator(beverage: Beverage) : BeverageDecorator(beverage) {
override fun cost() = super.cost() + 0.2
override fun description() = super.description() + " + sugar"
}
// Decorators can be composed freely
val order: Beverage = MilkDecorator(SugarDecorator(Coffee()))
// -> Coffee + sugar + milk
Facade #
Motivation: A subsystem has grown complex, with many interacting classes, and client code shouldn’t need to know about all of them.
Intent: Provide a simplified interface to a complex subsystem, reducing the number of dependencies that clients need to manage.

The facade doesn’t add new functionality—it just bundles a series of subsystem calls behind a convenient method so that client code remains simple and decoupled from the subsystem internals.
public class SmartHomeFacade(
private val lights: Lights,
private val thermostat: Thermostat,
private val musicPlayer: MusicPlayer,
) {
fun arriveHome() {
lights.on()
thermostat.setTemperature(22)
musicPlayer.playPlaylist("Welcome Home")
}
fun leaveHome() {
lights.off()
thermostat.setTemperature(16)
musicPlayer.stop()
}
}
// internal classes not exposed to the client
class Lights { /* ... */ }
class Thermostat { /* ... */ }
class MusicPlayer { /* ... */ }
Bridge #
Motivation: A class has two independent dimensions of variation (e.g., shape and colour), and combining them via inheritance would multiply the number of subclasses.
Intent: Separate an abstraction from its implementation so that the two can vary independently.

Instead of a class hierarchy that mixes both dimensions (e.g., RedCircle, BlueCircle, RedSquare, BlueSquare, …), the bridge lets each dimension have its own small hierarchy, connected by composition.
interface Renderer {
fun renderCircle(radius: Double)
fun renderSquare(side: Double)
}
class VectorRenderer : Renderer {
override fun renderCircle(radius: Double) { /* draw with vectors */ }
override fun renderSquare(side: Double) { /* draw with vectors */ }
}
class RasterRenderer : Renderer {
override fun renderCircle(radius: Double) { /* draw with pixels */ }
override fun renderSquare(side: Double) { /* draw with pixels */ }
}
abstract class Shape(protected val renderer: Renderer) {
abstract fun draw()
}
class Circle(renderer: Renderer, val radius: Double) : Shape(renderer) {
override fun draw() = renderer.renderCircle(radius)
}
class Square(renderer: Renderer, val side: Double) : Shape(renderer) {
override fun draw() = renderer.renderSquare(side)
}
Flyweight #
Motivation: The application needs a very large number of similar objects (e.g., characters in a text editor, trees in a game world), and storing each one independently would consume too much memory.
Intent: Share the common (intrinsic) state among multiple objects, while keeping the varying (extrinsic) state external, to reduce memory usage. A factory is used to create and maintain shared objects with the same intrinsic state.

For example, in a simulation of a forest with millions of trees, the tree type data (name, color, texture) is shared, while each tree’s position is stored separately.
class TreeType(val name: String, val color: String, val texture: String)
class TreeFactory {
private val types = mutableMapOf<String, TreeType>()
fun getTreeType(name: String, color: String, texture: String): TreeType =
types.getOrPut("$name-$color-$texture") {
TreeType(name, color, texture)
}
}
class Tree(val type: TreeType) {
fun draw(x: Double, y: Double) { /* ... */ }
}
Proxy #
Motivation: We need to control access to an object—for example, to defer its creation until it’s actually needed (lazy initialization), to check permissions, or to cache results.
Intent: Provide a surrogate or placeholder for another object to control access to it.

The proxy implements the same interface as the real object, so the client doesn’t know (or care) whether it’s talking to the real thing or the proxy. Common variants include lazy proxies, protection proxies, and caching proxies.
interface Image {
fun display()
}
class RealImage(private val filename: String) : Image {
init { loadFromDisk() }
private fun loadFromDisk() { /* expensive loading... */ }
override fun display() { /* render image */ }
}
class LazyImageProxy(private val filename: String) : Image {
private var realImage: RealImage? = null
override fun display() {
if (realImage == null) realImage = RealImage(filename)
realImage!!.display()
}
}