Observer pattern in Kotlin – Design patterns

Have you ever been subscribed to the newsletter? When the author publishes the new issue, it comes straight to your inbox. You can always choose to unsubscribe too.

Why am I asking? Chances are that you’re already familiar with the pattern I’m going to describe today – the observer pattern.

Pattern introduction

Observer pattern allows creating a structure that allows objects (observers or publishers) to inform about their state to change other objects (observables or subscribers or subjects) in a loosely coupled way.

Let’s rephrase the definition of the observer pattern basing on the famous GoF book:

Observer pattern – defines one-to-many relationship between objects in such a way, that when the object changes his state, then all of his dependent objects are notified and updated

Example – weather station

Let’s take a weather station as an example for the demonstration of the observer pattern.

So let’s consider there’s a station which measures the weather parameters. The station can have multiple displays (subscribers), which will be refreshed (notified) whenever the station changes its state. When the state of the displays will be updated, their goal is to display the freshest data.

Interfaces

At first, we’re going to provide common interfaces to describe the communication between the observer (publisher) and observables (subscribers) objects.

package kt.design.patterns.observer

internal interface Publisher<T>{
    fun register(subscriber: WeatherSubscriber)
    fun remove(subscriber: WeatherSubscriber)
    fun notifySubscribers()
}

interface WeatherSubscriber {
    fun update(temperature: Float, humidity: Float): WeatherSubscriber
}

internal interface DisplayElement {
    fun display(temperature: Float, humidity: Float)
}

As we can see, we have:

  • Publisher – this is the observer interface with the ability to register or remove (subscribe or unsubscribe) the observable objects and with the method for all
  • WeatherSubscriber – this is the observable interface with the update method
  • Display – additional interface defining displaying behavior

Implementation

And the implementation of the Publisher interface:

package kt.design.patterns.observer

internal data class WeatherStation(
    private val subscribers: ArrayList<WeatherSubscriber> = arrayListOf(),
    private val temperature: Float = 0F,
    private val humidity: Float = 0F
) : Publisher<WeatherStation> {

    override fun notifySubscribers() {
        subscribers.forEach {
            it.update(temperature, humidity)
        }
    }

    override fun register(subscriber: WeatherSubscriber) {
        subscribers.add(subscriber)
    }

    override fun remove(subscriber: WeatherSubscriber) {
        subscribers.remove(subscriber)
    }

    fun updateData(temperature: Float, humidity: Float): WeatherStation {
        return copy(temperature = temperature, humidity = humidity)
            .also { it.notifySubscribers() }
    }

}

CurrentConditionsDisplay class is the Subscriber and Display interfaces implementation:

package kt.design.patterns.observer

internal data class CurrentConditionsDisplay(
    private val name: String,
    private val temperature: Float = 0F,
    private val humidity: Float = 0F,
    private val station: WeatherStation
) : WeatherSubscriber, DisplayElement {

    override fun update(temperature: Float, humidity: Float): CurrentConditionsDisplay {
        val updatedConditions = copy(temperature = temperature, humidity = humidity)
        display(updatedConditions.temperature, updatedConditions.humidity)
        return updatedConditions
    }

    override fun display(temperature: Float, humidity: Float) {
        println("Current conditions on $name. Temperature: $temperature°C, humidity: ${humidity.toInt()}%")
    }
}

The observer in action – test

Let’s take a look at the simple test cases:

package kt.design.patterns.observer

import com.github.stefanbirkner.systemlambda.SystemLambda.tapSystemOut
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.io.ByteArrayOutputStream
import java.io.PrintStream

class CurrentConditionsDisplayTest {

    private val standardOut = System.out
    private val outputStreamCaptor = ByteArrayOutputStream()

    @BeforeEach
    fun setUp() {
        System.setOut(PrintStream(outputStreamCaptor))
    }

    @AfterEach
    fun tearDown() {
        System.setOut(standardOut)
    }

    @Test
    fun `should notify both displays with the measured weather data`() {
        val weatherStation = WeatherStation()
        val mobileDisplay = CurrentConditionsDisplay(station = weatherStation, name = "Mobile Display")
        val stationaryDisplay = CurrentConditionsDisplay(station = weatherStation, name = "Stationary Display")
        weatherStation.register(mobileDisplay)
        weatherStation.register(stationaryDisplay)

        val output = tapSystemOut {
            weatherStation.updateData(10F, 20F)
        }

        assertEquals(
            "Current conditions on Mobile Display. Temperature: 10.0°C, humidity: 20%\n"
                    + "Current conditions on Stationary Display. Temperature: 10.0°C, humidity: 20%",
            output.trim()
        )
    }

    @Test
    fun `should notify only one weather data when one of the subscribers is removed`() {
        val weatherStation = WeatherStation()
        val mobileDisplay = CurrentConditionsDisplay(station = weatherStation, name = "Mobile Display")
        val stationaryDisplay = CurrentConditionsDisplay(station = weatherStation, name = "Stationary Display")
        weatherStation.register(mobileDisplay)
        weatherStation.register(stationaryDisplay)
        weatherStation.notifySubscribers()
        weatherStation.remove(mobileDisplay)

        val output = tapSystemOut {
            weatherStation.updateData(20F, 90F)
        }

        assertEquals("Current conditions on Stationary Display. Temperature: 20.0°C, humidity: 90%", output.trim())
    }

    @Test
    fun `should not notify any object if there are no subscribers`() {
        val weatherStation = WeatherStation()

        val output = tapSystemOut {
            weatherStation.updateData(20F, 90F)
        }

        assertEquals(output.trim(), "")
    }
}

Try it yourself! As always the code is available on GitHub.

Drawbacks

The main disadvantage of this pattern is the necessity of the subscribers’ registration.

Ralph Johnson one of the GoF members describes it in the conference talk about the design patterns. He explains it with the example of the mobster and the FBI agent.
Does the former inform the latter when he’s going to rob a bank in the real-world scenario?

Conclusion

If you’re looking for a way to notify the objects about the changes then the observer pattern can be a good, loosely coupled solution.

Other ways to implement the Observer pattern in Kotlin:

  • using built-in delegate
  • using java.util.Observable interface

Resources

Leave a Reply

Your email address will not be published.