Behavioral Design Patterns - Kotlin Flashcards
Behavioral Design Patterns
In software engineering, behavioral design patterns are design patterns that identify common communication patterns between objects and realize these patterns. By doing so, these patterns increase flexibility in carrying out this communication.
Observer / Listener Pattern
The observer pattern is used to allow an object to publish changes to its state. Other objects subscribe to be immediately notified of any changes.
“Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically”.
Observer / Listener Pattern Example
Example :
interface TextChangedListener {
fun onTextChanged(oldText: String, newText: String) }
class PrintingTextChangedListener : TextChangedListener {
private var text = “”
override fun onTextChanged(oldText: String, newText: String) {
text = “Text is changed: $oldText -> $newText”
}
}
class TextView {
val listeners = mutableListOf()
var text: String by Delegates.observable(“”) { _, old, new ->
listeners.forEach { it.onTextChanged(old, new) }
}
}
Usage :
val textView = TextView().apply {
listener = PrintingTextChangedListener()
}
with(textView) {
text = “Lorem ipsum”
text = “dolor sit amet”
}
Output :
Text is changed -> Lorem ipsum
Text is changed Lorem ipsum -> dolor sit amet
Strategy Pattern
The strategy pattern is used to create an interchangeable family of algorithms from which the required process is chosen at run-time.
In the Strategy pattern, a class behaviour or its algorithm can be changed at run time. This type of design pattern comes under behaviour pattern.
In the Strategy pattern, we create objects which represent various strategies and a context object whose behaviour varies as per its strategy object. The strategy object changes the executing algorithm of the context object.
Strategy Pattern Example
Example :
class Printer(private val stringFormatterStrategy: (String) -> String) {
fun printString(string: String) { println(stringFormatterStrategy(string)) } }
val lowerCaseFormatter: (String) -> String = { it.toLowerCase() }
val upperCaseFormatter = { it: String -> it.toUpperCase() }
Usage :
val inputString = “LOREM ipsum DOLOR sit amet”
val lowerCasePrinter = Printer(lowerCaseFormatter)
lowerCasePrinter.printString(inputString)
val upperCasePrinter = Printer(upperCaseFormatter)
upperCasePrinter.printString(inputString)
val prefixPrinter = Printer { “Prefix: $it” }
prefixPrinter.printString(inputString)
Output :
lorem ipsum dolor sit amet
LOREM IPSUM DOLOR SIT AMET
Prefix: LOREM ipsum DOLOR sit amet
Command Pattern
The command pattern is used to express a request, including the call to be made and all of its required parameters, in a command object. The command may then be executed immediately or held for later use.
The Command design pattern is used to encapsulate a request as an object and pass to an invoker, wherein the invoker does not know how to service the request but uses the encapsulated command to perform an action.
Command Pattern Example
Example :
interface OrderCommand { fun execute() }
class OrderAddCommand(val id: Long) : OrderCommand { override fun execute() = println("Adding order with id: $id") }
class OrderPayCommand(val id: Long) : OrderCommand { override fun execute() = println("Paying for order with id: $id") }
class CommandProcessor {
private val queue = ArrayList()
fun addToQueue(orderCommand: OrderCommand): CommandProcessor = apply { queue.add(orderCommand) }
fun processCommands(): CommandProcessor = apply { queue.forEach { it.execute() } queue.clear() } }
Usage :
CommandProcessor()
.addToQueue(OrderAddCommand(1L))
.addToQueue(OrderAddCommand(2L))
.addToQueue(OrderPayCommand(2L))
.addToQueue(OrderPayCommand(1L))
.processCommands()
Output :
Adding order with id: 1
Adding order with id: 2
Paying for order with id: 2
Paying for order with id: 1
State Pattern
The state pattern is used to alter the behaviour of an object as its internal state changes. The pattern allows the class for an object to apparently change at run-time.
The State design pattern is used when an Object changes its behaviour based on its internal state.
If we have to change the behaviour of an object based on its state, we can have a state variable in the Object and use if-else condition block to perform different actions based on the state.
State pattern is used to provide a systematic and lose-coupled way to achieve this through Context and State implementations.
State Pattern Example
Example :
sealed class AuthorizationState
object Unauthorized : AuthorizationState()
class Authorized(val userName: String) : AuthorizationState()
class AuthorizationPresenter {
private var state: AuthorizationState = Unauthorized
val isAuthorized: Boolean
get() = when (state) {
is Authorized -> true
is Unauthorized -> false
}
val userName: String
get() {
val state = this.state //val enables smart casting of state
return when (state) {
is Authorized -> state.userName
is Unauthorized -> “Unknown”
}
}
fun loginUser(userName: String) { state = Authorized(userName) }
fun logoutUser() { state = Unauthorized }
override fun toString() = “User ‘$userName’ is logged in: $isAuthorized”
}
Usage :
val authorizationPresenter = AuthorizationPresenter()
authorizationPresenter.loginUser(“admin”)
println(authorizationPresenter)
authorizationPresenter.logoutUser()
println(authorizationPresenter)
Output :
User ‘admin’ is logged in: true
User ‘Unknown’ is logged in: false
Chain of Responsibility Pattern
The chain of responsibility pattern is used to process varied requests, each of which may be dealt with by a different handler.
Chain of responsibility pattern is used to achieve loose coupling in software design where a request from the client is passed to a chain of objects to process them. Later, the object in the chain will decide themselves who will be processing the request and whether the request is required to be sent to the next object in the chain or not.
Chain of Responsibility Pattern Example
Example :
interface HeadersChain { fun addHeader(inputHeader: String): String }
class AuthenticationHeader(val token: String?, var next: HeadersChain? = null) : HeadersChain {
override fun addHeader(inputHeader: String): String {
token ?: throw IllegalStateException(“Token should be not null”)
return inputHeader + “Authorization: Bearer $token\n”
.let { next?.addHeader(it) ?: it }
}
}
class ContentTypeHeader(val contentType: String, var next: HeadersChain? = null) : HeadersChain {
override fun addHeader(inputHeader: String): String =
inputHeader + “ContentType: $contentType\n”
.let { next?.addHeader(it) ?: it }
}
class BodyPayload(val body: String, var next: HeadersChain? = null) : HeadersChain {
override fun addHeader(inputHeader: String): String =
inputHeader + “$body”
.let { next?.addHeader(it) ?: it }
}
Usage :
//create chain elements val authenticationHeader = AuthenticationHeader("123456") val contentTypeHeader = ContentTypeHeader("json") val messageBody = BodyPayload("Body:\n{\n\"username\"=\"dbacinski\"\n}")
//construct chain authenticationHeader.next = contentTypeHeader contentTypeHeader.next = messageBody
//execute chain
val messageWithAuthentication =
authenticationHeader.addHeader(“Headers with Authentication:\n”)
println(messageWithAuthentication)
val messageWithoutAuth =
contentTypeHeader.addHeader(“Headers:\n”)
println(messageWithoutAuth)
Output :
Headers with Authentication:
Authorization: Bearer 123456
ContentType: json
Body:
{
“username”=”dbacinski”
}
Headers:
ContentType: json
Body:
{
“username”=”dbacinski”
}
Visitor Pattern
The visitor pattern is used to separate a relatively complex set of structured data classes from the functionality that may be performed upon the data that they hold.
The Visitor pattern is used when we have to perform an operation on a group of similar kind of Objects.
With the help of the visitor pattern, we can move the operational logic from the objects to another class.
Visitor Pattern Example
Example :
interface ReportVisitable { fun accept(visitor: ReportVisitor): R }
class FixedPriceContract(val costPerYear: Long) : ReportVisitable { override fun accept(visitor: ReportVisitor): R = visitor.visit(this) }
class TimeAndMaterialsContract(val costPerHour: Long, val hours: Long) : ReportVisitable { override fun accept(visitor: ReportVisitor): R = visitor.visit(this) }
class SupportContract(val costPerMonth: Long) : ReportVisitable { override fun accept(visitor: ReportVisitor): R = visitor.visit(this) }
interface ReportVisitor {
fun visit(contract: FixedPriceContract): R fun visit(contract: TimeAndMaterialsContract): R fun visit(contract: SupportContract): R }
class MonthlyCostReportVisitor : ReportVisitor {
override fun visit(contract: FixedPriceContract): Long =
contract.costPerYear / 12
override fun visit(contract: TimeAndMaterialsContract): Long =
contract.costPerHour * contract.hours
override fun visit(contract: SupportContract): Long =
contract.costPerMonth
}
class YearlyReportVisitor : ReportVisitor {
override fun visit(contract: FixedPriceContract): Long =
contract.costPerYear
override fun visit(contract: TimeAndMaterialsContract): Long =
contract.costPerHour * contract.hours
override fun visit(contract: SupportContract): Long =
contract.costPerMonth * 12
}
Usage :
val projectAlpha = FixedPriceContract(costPerYear = 10000)
val projectGamma = TimeAndMaterialsContract(hours = 150, costPerHour = 10)
val projectBeta = SupportContract(costPerMonth = 500)
val projectKappa = TimeAndMaterialsContract(hours = 50, costPerHour = 50)
val projects = arrayOf(projectAlpha, projectBeta, projectGamma, projectKappa)
val monthlyCostReportVisitor = MonthlyCostReportVisitor()
val monthlyCost = projects.map { it.accept(monthlyCostReportVisitor) }.sum()
println(“Monthly cost: $monthlyCost”)
assertThat(monthlyCost).isEqualTo(5333)
val yearlyReportVisitor = YearlyReportVisitor()
val yearlyCost = projects.map { it.accept(yearlyReportVisitor) }.sum()
println(“Yearly cost: $yearlyCost”)
assertThat(yearlyCost).isEqualTo(20000)
Output :
Monthly cost: 5333
Yearly cost: 20000
Mediator Pattern
Mediator design pattern is used to provide a centralized communication medium between different objects in a system. This pattern is very helpful in an enterprise application where multiple objects are interacting with each other.
“Mediator pattern is used to reduce communication complexity between multiple objects or classes”.
Mediator Pattern Example
Example :
class ChatUser(private val mediator: ChatMediator, val name: String) { fun send(msg: String) { println("$name: Sending Message= $msg") mediator.sendMessage(msg, this) }
fun receive(msg: String) { println("$name: Message received: $msg") } }
class ChatMediator {
private val users: MutableList = ArrayList()
fun sendMessage(msg: String, user: ChatUser) {
users
.filter { it != user }
.forEach {
it.receive(msg)
}
}
fun addUser(user: ChatUser): ChatMediator = apply { users.add(user) }
}
Usage :
val mediator = ChatMediator()
val john = ChatUser(mediator, “John”)
mediator
.addUser(ChatUser(mediator, “Alice”))
.addUser(ChatUser(mediator, “Bob”))
.addUser(john)
john.send(“Hi everyone!”)
Output :
John: Sending Message= Hi everyone!
Alice: Message received: Hi everyone!
Bob: Message received: Hi everyone!