1
头图

Preface

Design patterns will be encountered in interviews or work, but I often encounter small partners complaining that the chance of applying design patterns in actual work is very small.

I happened to encounter a observer mode to solve a problem in my recent work, and I would like to share it with you.

The background is as follows:

There are a few extra things that need to be done in the standard process of the user creating an order:

At the same time, these businesses are not fixed, and logic will be added or modified at any time based on business development.

If the logic is directly written in the ordering business, this "tuo" business that is not very core will take up more and more, and the modification may also affect the normal ordering process.

Of course, there are other solutions. For example, you can start several scheduled tasks, scan orders regularly, and then implement your own business logic; but this will waste many unnecessary requests.

Observer mode

Therefore, the observer pattern came into being, it is the event publisher sends a notification when its own state changes, and the observer obtains the message to implement the business logic.

In this way, the event publisher and receiver can be completely decoupled without affecting each other; in essence, it is also an implementation of the principle of opening and closing.

Sample code

Let's take a look at the interfaces and relationships used by the observer pattern first:

  • Main interface: defines the registration implementation and circular notification interface.
  • Observer interface: defines the interface for receiving notifications from the subject.
  • Both the subject and the observer interface can have multiple implementations.
  • The service code only needs to use the Subject.Nofity() interface.

Next, let’s take a look at an implementation case in the process of creating an order.

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

First, define two interfaces according to the above figure:

type Subject interface {
    Register(Observer)
    Notify(data interface{})
}

type Observer interface {
    Update(data interface{})
}

Since this is an event for OrderCreateSubject achieve Subject :

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 observers who have subscribed to the order event.

Next is to write the observer business logic, here I have implemented 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)
}

It is also very simple to use:

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

    create.Notify("abc123")
}

Output:

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

In this way, once we need to modify 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 into the entity.

The core process will no longer be modified.

Matching container

In fact, we can also omit the step of registering events, that is, use the container; the general process is as follows:

  1. All custom events are injected into the container.
  2. When registering events, take out all events from the container and register them 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 only need to use the Provide function provided by the container to register it in the container.

At the same time, in order for the container to support the existence of multiple instances of the same object, some new codes are also needed:

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"`
    }
)

In the observer interface, two new structures need to be added to store multiple instances of the same interface.

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

When creating a specific observer object, the Instance object is returned.

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

func NewB2() Instance {
    return Instance{
        Instance: &B2CreateOrder{},
    }
}
In fact, it is wrapped once with Instance.

In this way, when registering observers, all observer objects InstanceParams.Instances

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

In this way, the subject object is directly obtained from the container when in use, and then notified:

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

For more information about the usage of dig, please refer to the official document:

https://pkg.go.dev/go.uber.org/dig#hdr-Value_Groups

Summarize

Experienced developers will find that it is very similar to the publish-subscribe model, and of course their ideas are similar; we don’t have to struggle with the differences between the two (except during interviews); it’s more important to learn the ideas.


crossoverJie
5.4k 声望4k 粉丝