Structural Design Patterns in Object-Oriented Programming (OOP) focus on how classes and objects are composed to form larger structures. These patterns simplify the design by identifying how to realize relationships between entities, improving flexibility and reusability.
In this guide, we will explore the following structural design patterns:
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
Each pattern is explained with a Kotlin example that can be tested using the Kotlin Playground.
Adapter Pattern
The Adapter pattern allows incompatible interfaces to work together by converting the interface of a class into another interface the client expects.
Example
interface AmericanSocket {
fun plugIn(): String
}
class AmericanAppliance : AmericanSocket {
override fun plugIn() = "Plugged into American socket."
}
interface EuropeanSocket {
fun powerOn(): String
}
class EuropeanAppliance : EuropeanSocket {
override fun powerOn() = "Powered by European socket."
}
class SocketAdapter(private val europeanAppliance: EuropeanAppliance) : AmericanSocket {
override fun plugIn(): String = europeanAppliance.powerOn()
}
fun main() {
val americanAppliance = AmericanAppliance()
println(americanAppliance.plugIn())
val europeanAppliance = EuropeanAppliance()
val adapter = SocketAdapter(europeanAppliance)
println(adapter.plugIn()) // Using adapter to plug European appliance into an American socket
}
Bridge Pattern
The Bridge pattern separates an object’s abstraction from its implementation so the two can vary independently.
Example
interface Device {
fun turnOn(): String
fun turnOff(): String
}
class TV : Device {
override fun turnOn() = "TV turned on"
override fun turnOff() = "TV turned off"
}
class Radio : Device {
override fun turnOn() = "Radio turned on"
override fun turnOff() = "Radio turned off"
}
abstract class RemoteControl(protected val device: Device) {
abstract fun togglePower(): String
}
class BasicRemote(device: Device) : RemoteControl(device) {
private var isOn = false
override fun togglePower(): String {
isOn = !isOn
return if (isOn) device.turnOn() else device.turnOff()
}
}
fun main() {
val tv = TV()
val radio = Radio()
val tvRemote = BasicRemote(tv)
val radioRemote = BasicRemote(radio)
println(tvRemote.togglePower())
println(radioRemote.togglePower())
}
Composite Pattern
The Composite pattern is used to treat individual objects and compositions of objects uniformly. It creates a tree structure where individual objects and groups of objects can be treated the same way.
Example
interface Graphic {
fun draw(): String
}
class Circle : Graphic {
override fun draw() = "Drawing Circle"
}
class Square : Graphic {
override fun draw() = "Drawing Square"
}
class CompositeGraphic : Graphic {
private val graphics = mutableListOf<Graphic>()
fun add(graphic: Graphic) = graphics.add(graphic)
override fun draw(): String {
return graphics.joinToString(", ") { it.draw() }
}
}
fun main() {
val circle = Circle()
val square = Square()
val composite = CompositeGraphic()
composite.add(circle)
composite.add(square)
println(composite.draw())
}
Decorator Pattern
The Decorator pattern allows behavior to be added to individual objects, either statically or dynamically, without affecting the behavior of other objects from the same class.
Example
interface Coffee {
fun cost(): Double
}
class SimpleCoffee : Coffee {
override fun cost() = 2.0
}
class MilkDecorator(private val coffee: Coffee) : Coffee {
override fun cost() = coffee.cost() + 0.5
}
class SugarDecorator(private val coffee: Coffee) : Coffee {
override fun cost() = coffee.cost() + 0.3
}
fun main() {
val coffee = SimpleCoffee()
val coffeeWithMilk = MilkDecorator(coffee)
val coffeeWithMilkAndSugar = SugarDecorator(coffeeWithMilk)
println("Cost of plain coffee: ${coffee.cost()}")
println("Cost of coffee with milk: ${coffeeWithMilk.cost()}")
println("Cost of coffee with milk and sugar: ${coffeeWithMilkAndSugar.cost()}")
}
Facade Pattern
The Facade pattern provides a simplified interface to a complex subsystem, making it easier for the client to interact with the system.
Example
class CPU {
fun start() = "CPU started"
}
class Memory {
fun load() = "Memory loaded"
}
class HardDrive {
fun read() = "Hard drive read"
}
class ComputerFacade(private val cpu: CPU, private val memory: Memory, private val hardDrive: HardDrive) {
fun startComputer(): String {
return "${cpu.start()}, ${memory.load()}, ${hardDrive.read()}"
}
}
fun main() {
val computer = ComputerFacade(CPU(), Memory(), HardDrive())
println(computer.startComputer())
}
Flyweight Pattern
The Flyweight pattern reduces memory usage by sharing objects instead of creating new instances. This is useful when many objects are needed, but they share a common state.
Example
class Shape(val type: String) {
fun draw(): String = "Drawing $type"
}
class ShapeFactory {
private val shapes = mutableMapOf<String, Shape>()
fun getShape(type: String): Shape {
return shapes.getOrPut(type) { Shape(type) }
}
}
fun main() {
val factory = ShapeFactory()
val circle1 = factory.getShape("Circle")
val circle2 = factory.getShape("Circle")
val square = factory.getShape("Square")
println(circle1.draw())
println(circle2.draw())
println("Are circle1 and circle2 the same instance? ${circle1 === circle2}")
println(square.draw())
}
Proxy Pattern
The Proxy pattern provides a substitute or placeholder for another object, controlling access to it. Proxies are often used for lazy initialization or access control.
Example
interface Image {
fun display(): String
}
class RealImage(private val filename: String) : Image {
init {
println("Loading $filename")
}
override fun display() = "Displaying $filename"
}
class ProxyImage(private val filename: String) : Image {
private var realImage: RealImage? = null
override fun display(): String {
if (realImage == null) {
realImage = RealImage(filename)
}
return realImage!!.display()
}
}
fun main() {
val proxyImage = ProxyImage("photo.png")
println(proxyImage.display()) // Loads and displays the image
println(proxyImage.display()) // Just displays the image without loading
}
Conclusion
In this guide, we explored the following Structural Design Patterns:
- Adapter: Helps incompatible interfaces work together.
- Bridge: Decouples an abstraction from its implementation.
- Composite: Composes objects into tree structures and treats them uniformly.
- Decorator: Dynamically adds responsibilities to objects.
- Facade: Simplifies interactions with complex subsystems.
- Flyweight: Optimizes memory usage by sharing objects.
- Proxy: Controls access to another object.
Each of these patterns addresses a different structural need, making your code more flexible, reusable, and easier to maintain. Test these examples in a Kotlin playground to get a better understanding of how they work!
Comments
Post a Comment