Observer design pattern

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.

Description

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.

Objectives

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:

  • Provide an event-driven architecture where one event can trigger one or more actions
  • Uncouple the actions that are performed from the event that triggers them
  • Provide more than one event that triggers the same action

The notifier

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.

Acceptance criteria

The requirements must tell us to have some type that triggers some method in one or more actions:

  1. We must have a publisher with a NotifyObservers method that accepts a message as an argument and triggers a Notify method on every observer subscribed.
  2. We must have a method to add new subscribers to the publisher.
  3. We must have a method to remove new subscribers from the publisher.

Unit tests

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.

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:

  1. First, we need to use slice indexing to return a new slice containing every object from the beginning of the slice to the index we want to remove (not included).
  2. Then, we get another slice from the index we want to remove (not included) to the last object in the slice
  3. Finally, we join the previous two new slices into a new one (the 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.

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.189.157