In this article, we will learn what is the Observer design pattern and how to use it.
The use case
Let’s say we have a weather station that regularly takes measures of temperature and humidity:
struct WeatherData: Equatable {
var temperature: Int
var humidity: Int
}
class WeatherStation {
private var timer = Timer()
private var lastMeasurement: WeatherData? = nil
init() {
self.timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true, block: { [weak self] _ in
self?.measureWeather()
})
}
private func measureWeather() {
// Simulate new data
let newData = WeatherData(temperature: Int.random(in: 0..<10), humidity: Int.random(in: 0..<10))
if newData != lastMeasurement {
self.lastMeasurement = newData
}
}
}
And two screens that are connected to the station, one displaying temperature, the other one displaying humidity:
struct TemperatureScreen {
func display(temperature: Int) {
print("Temperature is now \(temperature)")
}
}
struct HumidityScreen {
func display(humidity: Int) {
print("Humidity is now \(humidity)")
}
}
→ How would we allow the several screens to update their information when the weather changes?
Polling the data?
One solution that can come to mind would be that a screen can regularly ask the weather station for new data, for instance, every 100 milliseconds.
Unfortunately, this won’t be very efficient. Here is why:
- If the weather doesn’t change during a minute, all screens would ask a lot of times the weather station for nothing, wasting energy.
- The more screens you have, the more the weather station will receive simultaneous requests, which risks to overload the station.
So instead of making the screens asking the new data, let’s try to see the solution in the other direction: what if it’s the weather station's responsibility to inform the screens of new data?
The observer pattern
The observer pattern is based on two elements:
- The observable (also called the subject): the object that is observed, the weather station.
- The observers (also called subscribers): they observe the observable. The screens observe the weather station.
The responsibility of the observable is to keep a list of its observers and notify them if needed. Then, the observers can register or unregister themselves from the observable.
We can see that like a newsletter: a person can subscribe to a newsletter, and the newsletter system will notify each subscriber when new information is available.
Let’s see what the Swift code would look like.
The code
First, we have to define the two protocols: the observable and the observer
Observable
The observable is composed of:
- A list of subscribers, of the type
WeatherStationSubscriber
(we will come to that). - A
register
method, that we can call to add subscribers. Anunregister
method should also be added to stop a subscription. - A
notify
method that will be used to notify the subscribers.
protocol WeatherStationObservable {
// 1
var subscribers: [WeatherStationSubscriber] { get }
// 2
func register(_ subscriber: WeatherStationSubscriber)
// 3
func notify(_ newData: WeatherData)
}
Let’s apply that to our weather station:
- We add the
WeatherStationObservable
protocol conformance to ourWeatherStation
. - We declare the list of subscribers.
- We implement the
register
method, by adding the new subscriber to the list of subscribers. Note that you may want to prevent the same observer to subscribe multiple times, by checking before if the subscriber is already in thesubscribers
, or by using a Set instead of a list. - Finally, we implement the
notify
method: we just have to iterate through each of the subscribers and notify them with the new data.
class WeatherStation: WeatherStationObservable { // 1
// previous code ...
// 2
var subscribers: [WeatherStationSubscriber] = []
// 3
func register(_ subscriber: WeatherStationSubscriber) {
subscribers.append(subscriber)
}
// 4
func notify(_ newData: WeatherData) {
for subscriber in subscribers {
// TODO: notify subscriber here !
}
}
}
Now we just have to modify the measureWeather
method to call the notify
method:
private func measureWeather() {
// Simulate new data
let newData = WeatherData(temperature: Int.random(in: 0..<10), humidity: Int.random(in: 0..<10))
if newData != lastMeasurement {
self.lastMeasurement = newData
self.notify(newData)
}
}
Observer
The observer (here we call it subscriber) is even easier: it just has to implement a method that can be called by the subscriber to notify it when new data is available:
protocol WeatherStationSubscriber {
func onNotified(_ newData: WeatherData)
}
Let’s apply that to one of our screens:
- Here the
onNotified
method will just send the temperature information to the display method.
struct TemperatureScreen: WeatherStationSubscriber {
// Previous code...
func onNotified(_ newData: WeatherData) {
display(temperature: newData.temperature)
}
}
Usage
Now let’s see how to use that in practice:
- We create the weather station.
- We create the screens.
- We register the screens to the weather station.
// 1
let weatherStation = WeatherStation()
// 2
let temperatureScreen = TemperatureScreen()
let humidityScreen = HumidityScreen()
// 3
weatherStation.register(temperatureScreen)
weatherStation.register(humidityScreen)
The final code
import Foundation
struct WeatherData: Equatable {
var temperature: Int
var humidity: Int
}
protocol WeatherStationObservable {
var subscribers: [WeatherStationSubscriber] { get }
func register(_ subscriber: WeatherStationSubscriber)
func notify(_ newData: WeatherData)
}
protocol WeatherStationSubscriber {
func onNotified(_ newData: WeatherData)
}
class WeatherStation: WeatherStationObservable {
private var timer = Timer()
private var lastMeasurement: WeatherData? = nil
var subscribers: [WeatherStationSubscriber] = []
init() {
self.timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true, block: { [weak self] _ in
self?.measureWeather()
})
}
private func measureWeather() {
print("Measuring...")
let newData = WeatherData(temperature: Int.random(in: 0..<10), humidity: Int.random(in: 0..<10))
if newData != lastMeasurement {
self.lastMeasurement = newData
notify(newData)
}
}
func register(_ subscriber: WeatherStationSubscriber) {
subscribers.append(subscriber)
}
func notify(_ newData: WeatherData) {
for subscriber in subscribers {
subscriber.onNotified(newData)
}
}
}
struct TemperatureScreen: WeatherStationSubscriber {
func onNotified(_ newData: WeatherData) {
display(temperature: newData.temperature)
}
func display(temperature: Int) {
print("Temperature is now \(temperature)")
}
}
struct HumidityScreen: WeatherStationSubscriber {
func onNotified(_ newData: WeatherData) {
display(humidity: newData.humidity)
}
func display(humidity: Int) {
print("Humidity is now \(humidity)")
}
}
let weatherStation = WeatherStation()
let temperatureScreen = TemperatureScreen()
let humidityScreen = HumidityScreen()
weatherStation.register(temperatureScreen)
weatherStation.register(humidityScreen)
Now if we run that code in a playground, we can see each time the weather station gets new data, it will notify the screens that will display the new weather information:
Measuring...
Temperature is now 7
Humidity is now 3
Measuring...
Temperature is now 1
Humidity is now 9
Measuring...
Temperature is now 4
Humidity is now 8
Wrap up
In this article, we learned what is the observer design pattern and how to use it.
I hope this article has been helpful to you. If you have any questions or feedback about this article, don’t hesitate to contact me on **Twitter!
Top comments (0)