🌱 DP: Observer
I think it would be a good idea to start with writing articles on well-known implementations of design patterns in a blog. Currently, while working with Golang, many developers neglect basic patterns, leading to the creation of redundant and hard-to-maintain code. I believe that shedding light on this topic will help improve code quality and increase development efficiency.
The Observer pattern is widely used in distributed systems, where communication between microservices is achieved through message publication and subscription. Such systems are typically implemented in a choreographic style, which allows them to work asynchronously.
If you have experience writing microservices in Golang, you may have noticed that the simplicity of creating and using Goroutines tends to lead developers towards using asynchronous styles.
Participants
Subject
Subject stores information about its Observers. The relationship between Observers and Subject is one-to-many, which means that the number of Observers can be unlimited. Therefore, the Subject interface should contain functions for attaching and detaching Observers, as well as for notifying Observers about a happening event.
The following text can be used to describe the Subject interface in code.
type Subject[Event any, Observer any] interface { Attach(observer *Observer) error Detach(observer *Observer) error Notify(event *Event) error }
Observer
Observer defines an interface for objects that need to be updated during certain events.
type Observer[Event any] interface { Update(event *Event) error }
However, the process of transforming incoming event into a new state of the object should be implemented by another participant of the pattern.
Change Manager
The ChangeManager interface should define the functionality that translates incoming messages into a new state of the object that needs to be updated by an Observer.
type ChangeManager[Event any, State any] interface { Convert(event *Event) (*State, error) }
Additionally, ChangeManager can be used to determine the logic of updating dependent observers from the subject.
Implementation
The main goal of implementation is to enable Subject to accept any number of Observers that can handle events of a generalized type. We can create different types of Observers, but we can only attach those that can understand events from Subject.
Additionally, we can assume that the structure of the incoming event may differ from the structure of the object's state. For example, let's assume that our incoming event is described by the Event
structure,
type Event struct { Id uuid.UUID `json:"id"` Message string `json:"message"` }
while the state of the object is described by the State
structure:
type State struct { Message string `json:"message"` }
Actually, the differences between structures can be significant, and developers may need to put in a lot of effort to transpose the Event
structure into the State
structure. However, since we are discussing the design pattern idea, let's present a simple implementation of the Convert
function in the ChangeManager interface, which creates a new state based on the received information from the event.
func (changeManager changeManager) Convert(event *event.Event) (*state.State, error) { ... return state.State{ Message: event.Message, }, nil }
One way is to integrate ChangeManager into Observer and use the Convert method inside the Update method. Thus, a specific implementation of the ChangeManager is defined for the Observer, which makes it easy to control the logic of updating objects when an event is received.
func (observer *observer) Update(event *event.Event) error { state, err := observer.changemanager.Convert(event) ... observer.object.state = *state return nil }
The Attach
, Detach
, and Notify
methods are trivial and based on processing lists of Observers that the Subject stores. I don't think it's worth going into detail about them in this section.
Next, we will look at an example of using our implementation. You can see for yourself how simple it is!
Example of usage
To start working with the pattern, we create a ChangeManager object that implements the conversion of incoming messages into the object's state, as described above.
changemanager, err := changemanager.New()
Then, we create an Observer and connect ChangeManager to it.
observer, err := observer.New("label observer", &changemanager)
Next, we need to create a Subject object and attach the prepared Observer to it.
subject, err := subject.New() ... subject.Attach(&observer)
If necessary, we can also easily disconnect the Observer.
subject.Detach(&observer)
Finally, we need to notify the connected Observers of the Subject about the new incoming event.
subject.Notify(&event.Event{ Id: uuid.New(), Message: "Update 1", })
Repository
Below is a repository containing code with an example of using the Observer pattern that you can study.
If you have downloaded the repository and want to see how the template works, simply enter the following command in the root directory of the repository.
make example-run
I used the Detroit style of unit-testing to test the code. I rejected the London style for the following reasons:
- The classic style is more resistant to changes in the code;
- Currently, tools such as gomock or mockery, which are used to generate mock objects, do not fully support generics (however, mockery works much better in this regard than gomock);
- Global use of mock objects can complicate test code.
To run the tests, enter the following command in the root directory of the repository:
make test