Structural Design Patterns - Kotlin Flashcards
Structural Design Patterns
In software engineering, structural design patterns are design patterns that ease the design by identifying a simple way to realize relationships between entities.
Adapter Pattern
The adapter pattern is used to provide a link between two otherwise incompatible types by wrapping the “adaptee” with a class that supports the interface required by the client.
Adapter pattern works as a bridge between two incompatible interfaces. This type of design pattern comes under structural pattern as this pattern combines the capability of two independent interfaces.
“The adapter pattern makes two incompatible interfaces compatible without changing their existing code”.
Adapter Pattern Example
Example :
interface Temperature {
var temperature: Double
}
class CelsiusTemperature(override var temperature: Double) : Temperature
class FahrenheitTemperature(var celsiusTemperature: CelsiusTemperature) : Temperature {
override var temperature: Double
get() = convertCelsiusToFahrenheit(celsiusTemperature.temperature)
set(temperatureInF) {
celsiusTemperature.temperature = convertFahrenheitToCelsius(temperatureInF)
}
private fun convertFahrenheitToCelsius(f: Double): Double = (f - 32) * 5 / 9
private fun convertCelsiusToFahrenheit(c: Double): Double = (c * 9 / 5) + 32
}
Usage:
val celsiusTemperature = CelsiusTemperature(0.0)
val fahrenheitTemperature = FahrenheitTemperature(celsiusTemperature)
celsiusTemperature.temperature = 36.6
println(“${celsiusTemperature.temperature} C -> ${fahrenheitTemperature.temperature} F”)
fahrenheitTemperature.temperature = 100.0
println(“${fahrenheitTemperature.temperature} F -> ${celsiusTemperature.temperature} C”)
Output :
- 6 C -> 97.88000000000001 F
- 0 F -> 37.77777777777778 C
Decorator Pattern
The decorator pattern is used to extend or alter the functionality of objects at run-time by wrapping them in an object of a decorator class. This provides a flexible alternative to using inheritance to modify behaviour.
The Decorator pattern allows a user to add new functionality to an existing object without altering its structure. This type of design pattern comes under structural pattern as this pattern acts as a wrapper to the existing class.
That is there will be additional features added to the original object. The component, which adds these additional features is called the Decorator.
The Decorator class in this pattern act as a wrapper object which dynamically attaches additional features to the original object at run-time.
“Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to sub-classing for extending functionality”.
Decorator Pattern Example
Example
interface CoffeeMachine { fun makeSmallCoffee() fun makeLargeCoffee() }
class NormalCoffeeMachine : CoffeeMachine { override fun makeSmallCoffee() = println("Normal: Making small coffee")
override fun makeLargeCoffee() = println(“Normal: Making large coffee”)
}
//Decorator: class EnhancedCoffeeMachine(val coffeeMachine: CoffeeMachine) : CoffeeMachine by coffeeMachine {
// overriding behaviour override fun makeLargeCoffee() { println("Enhanced: Making large coffee") coffeeMachine.makeLargeCoffee() }
// extended behaviour fun makeCoffeeWithMilk() { println("Enhanced: Making coffee with milk") coffeeMachine.makeSmallCoffee() println("Enhanced: Adding milk") } }
Usage :
val normalMachine = NormalCoffeeMachine()
val enhancedMachine = EnhancedCoffeeMachine(normalMachine)
// non-overridden behaviour
enhancedMachine.makeSmallCoffee()
// overriding behaviour
enhancedMachine.makeLargeCoffee()
// extended behaviour
enhancedMachine.makeCoffeeWithMilk()
Output :
Normal: Making small coffee
Enhanced: Making large coffee
Normal: Making large coffee
Enhanced: Making coffee with milk
Normal: Making small coffee
Enhanced: Adding milk
Facade Pattern
The facade pattern is used to define a simplified interface to a more complex subsystem.
Facade pattern hides the complexities of the system and provides an interface to the client using which the client can access the system. This type of design pattern comes under structural pattern as this pattern adds an interface to the existing system to hide its complexities.
This pattern involves a single class which provides simplified methods required by client and delegates calls to methods of the existing system classes.
Facade Pattern Example
Example:
class ComplexSystemStore(val filePath: String) {
init {
println(“Reading data from file: $filePath”)
}
val store = HashMap()
fun store(key: String, payload: String) { store.put(key, payload) }
fun read(key: String): String = store[key] ?: “”
fun commit() = println("Storing cached data: $store to file: $filePath") }
data class User(val login: String)
//Facade: class UserRepository { val systemPreferences = ComplexSystemStore("/data/default.prefs")
fun save(user: User) {
systemPreferences.store(“USER_KEY”, user.login)
systemPreferences.commit()
}
fun findFirst(): User = User(systemPreferences.read("USER\_KEY")) }
Usage :
val userRepository = UserRepository()
val user = User(“dbacinski”)
userRepository.save(user)
val resultUser = userRepository.findFirst()
println(“Found stored user: $resultUser”)
Output :
Reading data from file: /data/default.prefs
Storing cached data: {USER_KEY=dbacinski} to file: /data/default.prefs
Found stored user: User(login=dbacinski)
Protection Proxy Pattern
The proxy pattern is used to provide a surrogate or placeholder object, which references an underlying object. Protection proxy is restricting access.
The Proxy Pattern is used to create a representative object that controls access to another object, which may be remote, expensive to create or in need of being secured.
The Proxy can be very useful in controlling access to the original object, especially when objects should have different access rights.
In the Proxy Pattern, a client does not directly talk to the original object, it delegates it calls to the proxy object which calls the methods of the original object.
The important point is that the client does not know about the proxy, the proxy acts as an original object for the client.
Protection Proxy Pattern Example
Example :
interface File { fun read(name: String) }
class NormalFile : File { override fun read(name: String) = println("Reading file: $name") }
//Proxy: class SecuredFile : File { val normalFile = NormalFile() var password: String = ""
override fun read(name: String) {
if (password == “secret”) {
println(“Password is correct: $password”)
normalFile.read(name)
} else {
println(“Incorrect password. Access denied!”)
}
}
}
Usage:
val securedFile = SecuredFile()
securedFile.read(“readme.md”)
securedFile.password = “secret”
securedFile.read(“readme.md”)
Output:
Incorrect password. Access denied!
Password is correct: secret
Reading file: readme.md
Composite Pattern
The composite pattern is used to compose zero-or-more similar objects so that they can be manipulated as one object.
Composite pattern is a partitioning design pattern and describes a group of objects that are treated the same way as a single instance of the same type of object. The intent of a composite is to ‘compose’ objects into tree structures to represent part-whole hierarchies. It allows you to have a tree structure and ask each node in the tree structure to perform a task.
Composite Pattern Example
Example :
open class Equipment(private var price: Int, private var name: String) { open fun getPrice(): Int = price }
/*
[composite]
*/
open class Composite(name: String) : Equipment(0, name) { val equipments = ArrayList()
fun add(equipment: Equipment) { this.equipments.add(equipment) }
override fun getPrice(): Int {
return equipments.map { it.getPrice() }.sum()
}
}
/*
leafs
*/
class Cabbinet : Composite("cabbinet") class FloppyDisk : Equipment(70, "Floppy Disk") class HardDrive : Equipment(250, "Hard Drive") class Memory : Equipment(280, "Memory")
Usage:
var cabbinet = Cabbinet()
cabbinet.add(FloppyDisk())
cabbinet.add(HardDrive())
cabbinet.add(Memory())
println(cabbinet.getPrice())
Output:
600