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)
}
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
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: