Design patterns are encountered in both interviews and at work, but I often come across complaints from my partners that they have very little chance of applying design patterns in practice.

I have recently encountered a problem solving scenario using the Observer pattern at work, and I would like to share it with you.

The background is as follows.

Some additional things need to be done in the standard process after a user has created an order.

At the same time, these operations are not fixed, and logic can be added or modified at any time according to business development.

If the logic is written directly in the order placement business, this “heap” of not very core business will take up more and more space, and the normal order placement process may be affected when changes are made.

There are of course other options, such as starting a few timed tasks that regularly scan for orders and then implement their own business logic; however, this would waste many unnecessary requests.

Observer mode

The observer pattern is therefore born, where the event publisher notifies when its state changes and the observer gets the message to implement the business logic.

This allows the event publisher and receiver to be completely decoupled from each other; essentially an implementation of the open and closed principle.

Let’s start with a general look at the interfaces and relationships used by the Observer pattern.

  • Subject interface: defines the interface for registering implementations, circular notifications.
  • Observer interface: defines the interface for receiving notifications from the subject.
  • Both the subject and observer interfaces can have multiple implementations.
  • The business code only needs to use the Subject.Nofity() interface.

Next look at an example of the implementation of the order creation process.

The code is implemented in go and is similar in other languages.

Firstly, two interfaces are defined as per the above diagram.

1
2
3
4
5
6
7
type Subject interface {
	Register(Observer)
	Notify(data interface{})
}
type Observer interface {
	Update(data interface{})
}

Since we are placing an order, we have defined OrderCreateSubject to implement Subject.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
type OrderCreateSubject struct {
	observerList []Observer
}
func NewOrderCreate() Subject {
	return &OrderCreateSubject{}
}
func (o *OrderCreateSubject) Register(observer Observer) {
	o.observerList = append(o.observerList, observer)
}
func (o *OrderCreateSubject) Notify(data interface{}) {
	for _, observer := range o.observerList {
		observer.Update(data)
	}
}

The observerList slice is used to store all the observers who have subscribed to the order event.

Then it’s time to write the observer business logic, here I have implemented two.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type B1CreateOrder struct {
}
func (b *B1CreateOrder) Update(data interface{}) {
	fmt.Printf("b1.....data %v \n", data)
}
type B2CreateOrder struct {
}
func (b *B2CreateOrder) Update(data interface{}) {
	fmt.Printf("b2.....data %v \n", data)
}

It is also very simple to use.

1
2
3
4
5
6
func TestObserver(t *testing.T) {
	create := NewOrderCreate()
	create.Register(&B1CreateOrder{})
	create.Register(&B2CreateOrder{})
	create.Notify("abc123")
}

Output´╝Ü

1
2
b1.....data abc123 
b2.....data abc123
  1. create a subject subject of create order.
  2. Register all subscription events.
  3. Call the Notify method where a notification is required.

This way the individual event implementations will not affect each other should we need to change them, and even if we need to add other implementations it will be very easy to.

  1. write the implementation class.
  2. register into the entity.

No further changes are made to the core process.

With containers

In fact, we can also skip the step of registering events, that is, using containers; the general process is as follows.

  1. the custom events are all injected into the container.
  2. The place where the events are registered is where all the events are taken out of the container and registered one by one.

The container used here is https://github.com/uber-go/dig

In the modified code, whenever we add an observer (event subscription), we simply use the Provide function provided by the container to register it into the container.

Also, in order for the container to support multiple instances of the same object, a new section of code has to be added

Observer.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
type Observer interface {
	Update(data interface{})
}
type (
	Instance struct {
		dig.Out
		Instance Observer `group:"observers"`
	}
	InstanceParams struct {
		dig.In
		Instances []Observer `group:"observers"`
	}
)

Two new structures are needed in the observer interface to hold multiple instances of the same interface.

group: "observers" is used to declare that it is the same interface.

The Instance object is returned when the concrete observer object is created.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func NewB1() Instance {
	return Instance{
		Instance: &B1CreateOrder{},
	}
}
func NewB2() Instance {
	return Instance{
		Instance: &B2CreateOrder{},
	}
}

It’s actually wrapped in Instance for once.

This allows all observer objects to be retrieved from InstanceParams.Instances when an observer is registered.

1
2
3
4
5
err = c.Invoke(func(subject Subject, params InstanceParams) {
	for _, instance := range params.Instances {
		subject.Register(instance)
	}
})

This way, when used, the subject object is retrieved directly from the container and then notified of.

1
2
3
err = c.Invoke(func(subject Subject) {
	subject.Notify("abc123")
})

More information on the use of dig can be found in the official documentation at https://pkg.go.dev/go.uber.org/dig#hdr-Value_Groups

Summary

Experienced developers will find it very similar to the publish-subscribe model, but of course their thinking is similar; we don’t need to dwell on the differences (except in interviews); it’s more important to learn the thinking involved.


Reference https://crossoverjie.top/2021/09/02/go/observer/