preface

Design patterns can be encountered in interviews or in the workplace, but I often encounter friends who complain that they rarely apply design patterns in their actual work.

Recently, I met a problem solving scenario using observer mode in my work, and I would like to share it with you.

Here’s the background:

There are a few additional things that need to be done in the standard flow after the user creates an order:

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

If the logic is directly written in the order business, this “tuo” not very core business will occupy more and more, and the modification may affect the normal order process.

Of course, there are other solutions, such as starting several scheduled tasks, scanning orders periodically and implementing your own business logic; But this wastes a lot of unnecessary requests.

Observer model

Therefore, the observer pattern emerges as The Times require. It is the event publisher that sends a notification when its state changes, and the observer gets the message to realize the business logic.

In this way, event publishers and receivers can be completely decoupled from each other. In essence, it is also a realization of the open and closed principle.

The sample code

Let’s take a look at the interfaces and relationships used by the Observer pattern:

  • Main interface: defines the registration implementation, circular notification interface.
  • Observer interface: Defines the interface to receive notifications from principals.
  • Both the principal and observer interfaces can have multiple implementations.
  • The business code only needs to be usedSubject.Nofity()Interface.

Let’s take a look at an implementation example during the order creation process.

The code is implemented using GO, and other languages are similar.

First, two interfaces are defined according to the figure above:

type Subject interface {
	Register(Observer)
	Notify(data interface{})}type Observer interface {
	Update(data interface{})}Copy the code

Since this is an order event, we define OrderCreateSubject to implement Subject:

type OrderCreateSubject struct {
	observerList []Observer
}

func NewOrderCreate(a) 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)
	}
}
Copy the code

The observerList slice is used to hold all the observers subscribed to the order events.

The next step is to write the observer business logic, and here I implement two:

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)
}
Copy the code

It’s also very simple to use:

func TestObserver(t *testing.T) {
	create := NewOrderCreate()
	create.Register(&B1CreateOrder{})
	create.Register(&B2CreateOrder{})

	create.Notify("abc123")}Copy the code

The Output:

b1..... data abc123 b2..... data abc123Copy the code
  1. To create aCreate the orderThe main body ofsubject.
  2. Register all subscription events.
  3. Called where notification is requiredNotifyMethods.

This way, if we need to change the implementation of each event, it will not affect each other, even if it is very easy to add other implementations:

  1. Write the implementation class.
  2. Register an entity.

The core process is no longer modified.

Cooperate with the container

In fact, we can also omit the step of registering events, which is to use containers; The general process is as follows:

  1. Custom events are all injected into the container.
  2. The place to re-register events takes all the events out of the container and registers them one by one.

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

In the modified code, whenever we add an observer (event subscription), we just need to register with the container using the Provide function provided by the container.

In order for the container to support multiple instances of the same object, we also need to add some new code:

Observer.go:

type Observer interface {
	Update(data interface{})}type (
	Instance struct {
		dig.Out
		Instance Observer `group:"observers"`
	}

	InstanceParams struct {
		dig.In
		Instances []Observer `group:"observers"`})Copy the code

Two new constructs are required in the Observer interface to hold multiple instances of the same interface.

Group :”observers” is used to declare the same interface.

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

func NewB1(a) Instance {
	return Instance{
		Instance: &B1CreateOrder{},
	}
}

func NewB2(a) Instance {
	return Instance{
		Instance: &B2CreateOrder{},
	}
}
Copy the code

It’s just Instance wrapped once.

This allows you to fetch all observer objects from InstanceParams.instances when registering an observer.

	err = c.Invoke(func(subject Subject, params InstanceParams) {
		for _, instance := range params.Instances {
			subject.Register(instance)
		}
	})
Copy the code

Get the topic object directly from the container and notify it:

	err = c.Invoke(func(subject Subject) {
		subject.Notify("abc123")})Copy the code

More information about dig usage can be found in the official documentation:

Pkg.go.dev/go.uber.org…

conclusion

Experienced developers will find that the publish-subscribe model is very similar, but of course the thinking is similar; We don’t have to worry about the differences (except in interviews); It’s even more important to learn the ideas.