Skip to main content

Behavioral Design Patterns for Beginners

Photo by Safar Safarov on Unsplash


Behavioral Design Patterns in Object-Oriented Programming (OOP) are concerned with how objects interact and communicate with each other. They define the flow of control and responsibility among different objects in a flexible way. By using these patterns, you can enhance the way your software components collaborate.

In this guide, we will explore the following behavioral design patterns:

  • Chain of Responsibility
  • Command
  • Interpreter
  • Iterator
  • Mediator
  • Memento
  • Observer
  • State
  • Strategy
  • Template Method
  • Visitor

Each pattern is explained with a Kotlin example that can be tested using the Kotlin Playground.

Chain of Responsibility Pattern

The Chain of Responsibility pattern allows multiple objects to handle a request, passing the request along a chain until one of the objects handles it.

Example

abstract class Handler {
var next: Handler? = null

fun setNext(handler: Handler): Handler {
this.next = handler
return handler
}

abstract fun handleRequest(request: String)
}

class ConcreteHandlerA : Handler() {
override fun handleRequest(request: String) {
if (request == "A") {
println("Handled by ConcreteHandlerA")
} else {
next?.handleRequest(request)
}
}
}

class ConcreteHandlerB : Handler() {
override fun handleRequest(request: String) {
if (request == "B") {
println("Handled by ConcreteHandlerB")
} else {
next?.handleRequest(request)
}
}
}

fun main() {
val handlerA = ConcreteHandlerA()
val handlerB = ConcreteHandlerB()

handlerA.setNext(handlerB)
handlerA.handleRequest("A") // Handled by ConcreteHandlerA
handlerA.handleRequest("B") // Handled by ConcreteHandlerB
handlerA.handleRequest("C") // Not handled
}

Command Pattern

The Command pattern turns a request into an object, allowing for parameterization of clients with different requests, queuing of requests, and logging.

Example

interface Command {
fun execute()
}

class Light {
fun turnOn() = println("Light turned on")
fun turnOff() = println("Light turned off")
}

class TurnOnCommand(private val light: Light) : Command {
override fun execute() = light.turnOn()
}

class TurnOffCommand(private val light: Light) : Command {
override fun execute() = light.turnOff()
}

class RemoteControl {
private val commands = mutableListOf<Command>()

fun addCommand(command: Command) {
commands.add(command)
}

fun executeCommands() {
commands.forEach { it.execute() }
commands.clear()
}
}

fun main() {
val light = Light()
val turnOnCommand = TurnOnCommand(light)
val turnOffCommand = TurnOffCommand(light)
val remoteControl = RemoteControl()

remoteControl.addCommand(turnOnCommand)
remoteControl.addCommand(turnOffCommand)

remoteControl.executeCommands()
}

Interpreter Pattern

The Interpreter pattern defines a representation for a language’s grammar and provides an interpreter to evaluate sentences in the language.

Example

interface Expression {
fun interpret(context: String): Boolean
}

class TerminalExpression(private val data: String) : Expression {
override fun interpret(context: String): Boolean {
return context.contains(data)
}
}

class OrExpression(private val expr1: Expression, private val expr2: Expression) : Expression {
override fun interpret(context: String): Boolean {
return expr1.interpret(context) || expr2.interpret(context)
}
}

fun main() {
val isMale = OrExpression(TerminalExpression("John"), TerminalExpression("Mike"))

println(isMale.interpret("John is here")) // true
println(isMale.interpret("Sarah is here")) // false
}

Iterator Pattern

The Iterator pattern provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

Example

class NameCollection(private val names: List<String>) {
fun iterator(): Iterator<String> {
return names.iterator()
}
}

fun main() {
val collection = NameCollection(listOf("John", "Jane", "Mike"))
val iterator = collection.iterator()

while (iterator.hasNext()) {
println(iterator.next())
}
}

Mediator Pattern

The Mediator pattern defines an object that controls how a set of objects interact, centralizing communication and reducing dependencies between them.

Example

interface Mediator {
fun send(message: String, colleague: Colleague)
}

abstract class Colleague(protected val mediator: Mediator) {
abstract fun receive(message: String)
}

class ConcreteColleagueA(mediator: Mediator) : Colleague(mediator) {
fun sendMessage(message: String) {
mediator.send(message, this)
}

override fun receive(message: String) {
println("Colleague A received: $message")
}
}

class ConcreteColleagueB(mediator: Mediator) : Colleague(mediator) {
override fun receive(message: String) {
println("Colleague B received: $message")
}
}

class ConcreteMediator : Mediator {
lateinit var colleagueA: ConcreteColleagueA
lateinit var colleagueB: ConcreteColleagueB

override fun send(message: String, colleague: Colleague) {
if (colleague == colleagueA) {
colleagueB.receive(message)
} else {
colleagueA.receive(message)
}
}
}

fun main() {
val mediator = ConcreteMediator()
val colleagueA = ConcreteColleagueA(mediator)
val colleagueB = ConcreteColleagueB(mediator)

mediator.colleagueA = colleagueA
mediator.colleagueB = colleagueB

colleagueA.sendMessage("Hello from A")
}

Memento Pattern

The Memento pattern captures and externalizes an object’s internal state, allowing it to be restored later without exposing its internals.

Example

class Memento(val state: String)

class Originator {
var state: String = ""

fun save(): Memento {
return Memento(state)
}

fun restore(memento: Memento) {
state = memento.state
}
}

fun main() {
val originator = Originator()
originator.state = "State #1"

val savedState = originator.save()
originator.state = "State #2"
println("Current State: ${originator.state}")

originator.restore(savedState)
println("Restored State: ${originator.state}")
}

Observer Pattern

The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

Example

interface Observer {
fun update(message: String)
}

class ConcreteObserver(private val name: String) : Observer {
override fun update(message: String) {
println("$name received: $message")
}
}

class Subject {
private val observers = mutableListOf<Observer>()

fun addObserver(observer: Observer) {
observers.add(observer)
}

fun notifyObservers(message: String) {
observers.forEach { it.update(message) }
}
}

fun main() {
val subject = Subject()
val observer1 = ConcreteObserver("Observer 1")
val observer2 = ConcreteObserver("Observer 2")

subject.addObserver(observer1)
subject.addObserver(observer2)

subject.notifyObservers("Update 1")
}

State Pattern

The State pattern allows an object to alter its behavior when its internal state changes. The object will appear to change its class.

Example

interface State {
fun handle()
}

class ConcreteStateA : State {
override fun handle() = println("State A handling request")
}

class ConcreteStateB : State {
override fun handle() = println("State B handling request")
}

class Context(var state: State) {
fun request() = state.handle()
}

fun main() {
val context = Context(ConcreteStateA())

context.request() // State A handling request
context.state = ConcreteStateB()
context.request() // State B handling request
}

Strategy Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.

Example

interface Strategy {
fun execute(a: Int, b: Int): Int
}

class AddStrategy : Strategy {
override fun execute(a: Int, b: Int) = a + b
}

class SubtractStrategy : Strategy {
override fun execute(a: Int, b: Int) = a - b
}

class Context(private var strategy: Strategy) {
fun setStrategy(strategy: Strategy) {
this.strategy = strategy
}

fun executeStrategy(a: Int, b: Int): Int {
return strategy.execute(a, b)
}
}

fun main() {
val context = Context(AddStrategy())
println("Addition: ${context.executeStrategy(5, 3)}") // 8

context.setStrategy(SubtractStrategy())
println("Subtraction: ${context.executeStrategy(5, 3)}") // 2
}

Template Method Pattern

The Template Method pattern defines the skeleton of an algorithm, deferring some steps to subclasses without changing the overall structure.

Example

abstract class Game {
abstract fun initialize()
abstract fun startPlay()
abstract fun endPlay()

fun play() {
initialize()
startPlay()
endPlay()
}
}

class Football : Game() {
override fun initialize() = println("Football Game Initialized")
override fun startPlay() = println("Football Game Started")
override fun endPlay() = println("Football Game Finished")
}

fun main() {
val game = Football()
game.play()
}

Visitor Pattern

The Visitor pattern allows adding new operations to objects without modifying them by letting a “visitor” object perform the operations.

Example

interface Visitable {
fun accept(visitor: Visitor)
}

class ConcreteElementA : Visitable {
override fun accept(visitor: Visitor) {
visitor.visit(this)
}
}

class ConcreteElementB : Visitable {
override fun accept(visitor: Visitor) {
visitor.visit(this)
}
}

interface Visitor {
fun visit(element: ConcreteElementA)
fun visit(element: ConcreteElementB)
}

class ConcreteVisitor : Visitor {
override fun visit(element: ConcreteElementA) = println("Visited Element A")
override fun visit(element: ConcreteElementB) = println("Visited Element B")
}

fun main() {
val elements = listOf(ConcreteElementA(), ConcreteElementB())
val visitor = ConcreteVisitor()

elements.forEach { it.accept(visitor) }
}

Conclusion

In this guide, we explored Behavioral Design Patterns with Kotlin examples:

  1. Chain of Responsibility: Passes requests along a chain until handled.
  2. Command: Encapsulates a request as an object.
  3. Interpreter: Interprets a language.
  4. Iterator: Provides a way to traverse collections.
  5. Mediator: Centralizes communication between objects.
  6. Memento: Captures and restores object state.
  7. Observer: Notifies multiple objects of state changes.
  8. State: Alters behavior based on an object’s state.
  9. Strategy: Encapsulates algorithms and makes them interchangeable.
  10. Template Method: Defines an algorithm’s skeleton with customizable steps.
  11. Visitor: Adds new operations to objects without modifying them.

These patterns enhance communication between objects in a flexible way. Test these patterns in a Kotlin playground for hands-on experience.

Comments

Popular posts from this blog

Understanding Number Systems: Decimal, Binary, and Hexadecimal

In everyday life, we use numbers all the time, whether for counting, telling time, or handling money. The number system we’re most familiar with is the   decimal system , but computers use other systems, such as   binary   and   hexadecimal . Let’s break down these number systems to understand how they work. What is a Number System? A number system is a way of representing numbers using a set of symbols and rules. The most common number systems are: Decimal (Base 10) Binary (Base 2) Hexadecimal (Base 16) Each system has a different “base” that tells us how many unique digits (symbols) are used to represent numbers. Decimal Number System (Base 10) This is the system we use daily. It has  10 digits , ranging from  0 to 9 . Example: The number  529  in decimal means: 5 × 1⁰² + 2 × 1⁰¹ + 9 × 1⁰⁰ =  500 + 20 + 9 = 529 Each position represents a power of 10, starting from the rightmost digit. Why Base 10? Decimal is base 10 because it has 10 digits...

How to Monetize Your API as an Individual Developer While Hosting on Your Own Server?

In the API economy, cloud services like AWS, Google Cloud, and Azure offer many conveniences, such as scaling and infrastructure management. However, some developers prefer more control and autonomy, opting to host their APIs on personal servers. Whether for cost efficiency, data privacy, or customization, hosting your own API comes with both advantages and challenges. But, even without cloud platforms, there are effective ways to monetize your API. This guide will explore how individual developers can successfully monetize their APIs while hosting them on their own servers. Why Host Your API on Your Own Server? Hosting your own API gives you full control over the infrastructure and potentially lower long-term costs. Here’s why some developers choose this approach: Cost Control : Instead of paying ongoing cloud fees, you may opt for a one-time or lower-cost hosting solution that fits your budget and resource needs. Data Ownership : You have full control over data, which is critical if ...

API Testing with Jest and Supertest: A Step-by-Step Guide

API testing is essential to ensure your endpoints behave as expected across all scenarios. In this guide, we’ll explore how to use Jest and Supertest to test a sample API with various response types, including success, authentication errors, and validation errors. By the end, you’ll understand how to apply these tools to check for different response structures and status codes. 0. Prerequisites: Setting Up Your Environment Before diving into API testing, it’s important to ensure that your development environment is properly set up. Here’s what you need to do: Step 1: Install Node.js and npm Node.js  is a JavaScript runtime that allows you to run JavaScript code on the server side. It comes with  npm  (Node Package Manager), which helps you install and manage packages. Installation Steps: Download and install Node.js from the  official website . To verify the installation, open your terminal and run: node -v npm -v This should display the installed versions of Node.js...

The Weight of Responsibility: A Developer’s Journey to Balance Passion and Reality

For the past several years, Eddie has been on a steady climb in his career as a developer, but recently, he found himself at a crossroads — caught between the weight of his responsibilities and the desire to pursue his true passions. His journey began with a three-month internship as a web developer, which led to nearly four years in an application developer role. After that, he spent almost a year as a systems associate, managing tasks across systems analysis, quality assurance, and business analysis. Eventually, he returned to full-time software development for another two years before transitioning into more complex roles. For over a year, he worked as a multi-role software developer and database administrator before stepping into his current position as a senior software developer, database administrator, and cloud administrator — occasionally handling security tasks as well. Now, with over 8 years of professional experience, he also leads a small team of developers, which has been...

Avoiding Confusion in API Design: The Importance of Clear Responses

In today’s fast-paced software development landscape, APIs play a crucial role in connecting services and enabling functionality. However, poor design choices can lead to confusion and inefficiency for both developers and users. One such choice is the omission of a response body for successful requests, a practice I recently encountered in an enterprise API designed for bill payments. The Case of the No-Response API The API in question serves two main endpoints: one for inquiring about account validity and another for confirming payment. When successful, the API returned a  200 OK  status but no response body. This design choice led to significant confusion during our integration process. Even the internal team who developed the said API struggled to justify this approach, revealing a lack of clarity around the rationale behind it. Pros of This Design Choice While the intention behind this design may have been to streamline responses, several potential benefits can be identifi...