We will finish the common Gang of Four design patterns with my favorite: the Observer pattern, also known as publish/subscriber or publish/listener. With the State pattern, we defined our first event-driven architecture, but with the Observer pattern we will really reach a new level of abstraction.
The idea behind the Observer pattern is simple--to subscribe to some event that will trigger some behavior on many subscribed types. Why is this so interesting? Because we uncouple an event from its possible handlers.
For example, imagine a login button. We could code that when the user clicks the button, the button color changes, an action is executed, and a form check is performed in the background. But with the Observer pattern, the type that changes the color will subscribe to the event of the clicking of the button. The type that checks the form and the type that performs an action will subscribe to this event too.
The Observer pattern is especially useful to achieve many actions that are triggered on one event. It is also especially useful when you don't know how many actions are performed after an event in advance or there is a possibility that the number of actions is going to grow in the near future. To resume, do the following:
We will develop the simplest possible application to fully understand the roots of the Observer pattern. We are going to make a Publisher
struct, which is the one that triggers an event so it must accept new observers and remove them if necessary. When the Publisher
struct is triggered, it must notify all its observers of the new event with the data associated.
The requirements must tell us to have some type that triggers some method in one or more actions:
NotifyObservers
method that accepts a message as an argument and triggers a Notify
method on every observer subscribed.Maybe you have realized that our requirements defined almost exclusively the Publisher
type. This is because the action performed by the observer is irrelevant for the Observer pattern. It should simply execute an action, in this case the Notify
method, that one or many types will implement. So let's define this only interface for this pattern:
type Observer interface { Notify(string) }
The Observer
interface has a Notify
method that accepts a string
type that will contain the message to spread. It does not need to return anything, but we could return an error if we want to check if all observers have been reached when calling the publish
method of the Publisher
structure.
To test all the acceptance criteria, we just need a structure called Publisher
with three methods:
type Publisher struct { ObserversList []Observer } func (s *Publisher) AddObserver(o Observer) {} func (s *Publisher) RemoveObserver(o Observer) {} func (s *Publisher) NotifyObservers(m string) {}
The Publisher
structure stores the list of subscribed observers in a slice field called ObserversList
. Then it has the three methods mentioned on the acceptance criteria-the AddObserver
method to subscribe a new observer to the publisher, the RemoveObserver
method to unsubscribe an observer, and the NotifyObservers
method with a string that acts as the message we want to spread between all observers.
With these three methods, we have to set up a root test to configure the Publisher
and three subtests to test each method. We also need to define a test type structure that implements the Observer
interface. This structure is going to be called TestObserver
:
type TestObserver struct { ID int Message string } func (p *TestObserver) Notify(m string) { fmt.Printf("Observer %d: message '%s' received ", p.ID, m) p.Message = m }
The TestObserver
structure implements the Observer pattern by defining a Notify(string)
method in its structure. In this case, it prints the received message together with its own observer ID. Then, it stores the message in its Message
field. This allows us to check later if the content of the Message
field is as expected. Remember that it could also be done by passing the testing.T
pointer and the expected message and checking within the TestObserver
structure.
Now we can set up the Publisher
structure to execute the three tests. We will create three instances of the TestObserver
structure:
func TestSubject(t *testing.T) { testObserver1 := &TestObserver{1, ""} testObserver2 := &TestObserver{2, ""} testObserver3 := &TestObserver{3, ""} publisher := Publisher{}
We have given a different ID to each observer so that we can see later that each of them has printed the expected message. Then, we have added the observers by calling the AddObserver
method on the Publisher
structure.
Let's write an AddObserver
test, it must add a new observer to the ObserversList
field of the Publisher
structure:
t.Run("AddObserver", func(t *testing.T) { publisher.AddObserver(testObserver1) publisher.AddObserver(testObserver2) publisher.AddObserver(testObserver3) if len(publisher.ObserversList) != 3 { t.Fail() } })
We have added three observers to the Publisher
structure, so the length of the slice must be 3. If it's not 3, the test will fail.
The RemoveObserver
test will take the observer with ID 2 and remove it from the list:
t.Run("RemoveObserver", func(t *testing.T) { publisher.RemoveObserver(testObserver2) if len(publisher.ObserversList) != 2 { t.Errorf("The size of the observer list is not the " + "expected. 3 != %d ", len(publisher.ObserversList)) } for _, observer := range publisher.ObserversList { testObserver, ok := observer.(TestObserver) if !ok { t.Fail() } if testObserver.ID == 2 { t.Fail() } } })
After removing the second observer, the length of the Publisher
structure must be 2 now. We also check that none of the observers left have the ID
2 because it must be removed.
The last method to test is the Notify
method. When using the Notify
method, all instances of TestObserver
structure must change their Message
field from empty to the passed message (Hello World!
in this case). First we will check that all the Message
fields are, in fact, empty before calling the NotifyObservers
test:
t.Run("Notify", func(t *testing.T) { for _, observer := range publisher.ObserversList { printObserver, ok := observer.(*TestObserver) if !ok { t.Fail() break } if printObserver.Message != "" { t.Errorf("The observer's Message field weren't " + " empty: %s ", printObserver.Message) } }
Using a for
statement, we are iterating over the ObserversList
field to slice in the publisher
instance. We need to make a type casting from a pointer to an observer, to a pointer to the TestObserver
structure, and check that the casting has been done correctly. Then, we check that the Message
field is actually empty.
The next step is to create a message to send--in this case, it will be "Hello World!"
and then pass this message to the NotifyObservers
method to notify every observer on the list (currently observers 1 and 3 only):
... message := "Hello World!" publisher.NotifyObservers(message) for _, observer := range publisher.ObserversList { printObserver, ok := observer.(*TestObserver) if !ok { t.Fail() break } if printObserver.Message != message { t.Errorf("Expected message on observer %d was " + "not expected: '%s' != '%s' ", printObserver.ID, printObserver.Message, message) } } }) }
After calling the NotifyObservers
method, each TestObserver
tests in the ObserversList
field must have the message "Hello World!"
stored in their Message
field. Again, we use a for
loop to iterate over every observer of the ObserversList
field and we typecast each to a TestObserver
test (remember that TestObserver
structure doesn't have any field as it's an interface). We could avoid type casting by adding a new Message()
method to Observer
instance and implementing it in the TestObserver
structure to return the contents of the Message
field. Both methods are equally valid. Once we have type casted to a TestObserver
method called printObserver
variable as a local variable, we check that each instance in the ObserversList
structure has the string "Hello World!"
stored in their Message
field.
Time to run the tests that must fail all to check their effectiveness in the later implementation:
go test -v
=== RUN TestSubject
=== RUN TestSubject/AddObserver
=== RUN TestSubject/RemoveObserver
=== RUN TestSubject/Notify
--- FAIL: TestSubject (0.00s)
--- FAIL: TestSubject/AddObserver (0.00s)
--- FAIL: TestSubject/RemoveObserver (0.00s)
observer_test.go:40: The size of the observer list is not the expected. 3 != 0
--- PASS: TestSubject/Notify (0.00s)
FAIL
exit status 1
FAIL
Something isn't working as expected. How is the Notify
method passing the tests if we haven't implemented the function yet? Take a look at the test of the Notify
method again. The test iterates over the ObserversList
structure and each F
ail
call is inside this for loop. If the list is empty, it won't iterate, so it won't execute any Fail call
.
Let's fix this issue by adding a small non-empty list check at the beginning of the Notify
test:
if len(publisher.ObserversList) == 0 { t.Errorf("The list is empty. Nothing to test ") }
And we will rerun the tests to see if the TestSubject/Notify
method is already failing:
go test -v === RUN TestSubject === RUN TestSubject/AddObserver === RUN TestSubject/RemoveObserver === RUN TestSubject/Notify --- FAIL: TestSubject (0.00s) --- FAIL: TestSubject/AddObserver (0.00s) --- FAIL: TestSubject/RemoveObserver (0.00s) observer_test.go:40: The size of the observer list is not the expected. 3 != 0 --- FAIL: TestSubject/Notify (0.00s) observer_test.go:58: The list is empty. Nothing to test FAIL exit status 1 FAIL
Nice, all of them are failing and now we have some guarantee on our tests. We can proceed to the implementation.
Our implementation is just to define the AddObserver
, the RemoveObserver
, and the NotifyObservers
methods:
func (s *Publisher) AddObserver(o Observer) { s.ObserversList = append(s.ObserversList, o) }
The AddObserver
method adds the Observer
instance to the ObserversList
structure by appending the pointer to the current list of pointers. This one was very easy. The AddObserver
test must be passing now (but not the rest or we could have done something wrong):
go test -v === RUN TestSubject === RUN TestSubject/AddObserver === RUN TestSubject/RemoveObserver === RUN TestSubject/Notify --- FAIL: TestSubject (0.00s) --- PASS: TestSubject/AddObserver (0.00s) --- FAIL: TestSubject/RemoveObserver (0.00s) observer_test.go:40: The size of the observer list is not the expected. 3 != 3 --- FAIL: TestSubject/Notify (0.00s) observer_test.go:87: Expected message on observer 1 was not expected: 'default' != 'Hello World!' observer_test.go:87: Expected message on observer 2 was not expected: 'default' != 'Hello World!' observer_test.go:87: Expected message on observer 3 was not expected: 'default' != 'Hello World!' FAIL exit status 1 FAIL
Excellent. Just the AddObserver
method has passed the test, so we can now continue to the RemoveObserver
method:
func (s *Publisher) RemoveObserver(o Observer) { var indexToRemove int for i, observer := range s.ObserversList { if observer == o { indexToRemove = i break } } s.ObserversList = append(s.ObserversList[:indexToRemove], s.ObserversList[indexToRemove+1:]...) }
The RemoveObserver
method will iterate for each element in the ObserversList
structure, comparing the Observer
object's o
variable with the ones stored in the list. If it finds a match, it saves the index in the local variable, indexToRemove
, and stops the iteration. The way to remove indexes on a slice in Go is a bit tricky:
append
function)For example, in a list from 1 to 10 in which we want to remove the number 5, we have to create a new slice, joining a slice from 1 to 4 and a slice from 6 to 10.
This index removal is done with the append
function again because we are actually appending two lists together. Just take a closer look at the three dots at the end of the second argument of the append
function. The append
function adds an element (the second argument) to a slice (the first), but we want to append an entire list. This can be achieved using the three dots, which translate to something like keep adding elements until you finish the second array.
Ok, let's run this test now:
go test -v
=== RUN TestSubject
=== RUN TestSubject/AddObserver
=== RUN TestSubject/RemoveObserver
=== RUN TestSubject/Notify
--- FAIL: TestSubject (0.00s)
--- PASS: TestSubject/AddObserver (0.00s)
--- PASS: TestSubject/RemoveObserver (0.00s)
--- FAIL: TestSubject/Notify (0.00s)
observer_test.go:87: Expected message on observer 1 was not expected: 'default' != 'Hello World!'
observer_test.go:87: Expected message on observer 3 was not expected: 'default' != 'Hello World!'
FAIL
exit status 1
FAIL
We continue in the good path. The RemoveObserver
test has been fixed without fixing anything else. Now we have to finish our implementation by defining the NotifyObservers
method:
func (s *Publisher) NotifyObservers(m string) { fmt.Printf("Publisher received message '%s' to notify observers ", m) for _, observer := range s.ObserversList { observer.Notify(m) } }
The NotifyObservers
method is quite simple because it prints a message to the console to announce that a particular message is going to be passed to the Observers
. After this, we use a for loop to iterate over ObserversList
structure and execute each Notify(string)
method by passing the argument m
. After executing this, all observers must have the message Hello World!
stored in their Message
field. Let's see if this is true by running the tests:
go test -v
=== RUN TestSubject
=== RUN TestSubject/AddObserver
=== RUN TestSubject/RemoveObserver
=== RUN TestSubject/Notify
Publisher received message 'Hello World!' to notify observers
Observer 1: message 'Hello World!' received
Observer 3: message 'Hello World!' received
--- PASS: TestSubject (0.00s)
--- PASS: TestSubject/AddObserver (0.00s)
--- PASS: TestSubject/RemoveObserver (0.00s)
--- PASS: TestSubject/Notify (0.00s)
PASS
ok
Excellent! We can also see the outputs of the Publisher
and Observer
types on the console. The Publisher
structure prints the following message:
hey! I have received the message 'Hello World!' and I'm going to pass the same message to the observers
After this, all observers print their respective messages as follows:
hey, I'm observer 1 and I have received the message 'Hello World!'
And the same for the third observer.
We have unlocked the power of event-driven architectures with the State pattern and the Observer pattern. Now you can really execute asynchronous algorithms and operations in your application that respond to events in your system.
The Observer pattern is commonly used in UI's. Android programming is filled with Observer patterns so that the Android SDK can delegate the actions to be performed by the programmers creating an app.
3.133.151.220