Structural patterns of 23 design patterns implemented in go language

Time:2021-12-4

Summary:This paper mainly focuses on structural pattern. Its main idea is to assemble multiple objects into a larger structure, maintain the flexibility and efficiency of the structure, and solve the coupling problem between modules from the structure of the program.

This article is shared from the Huawei cloud community “come on, here are the go language implementations of 23 design patterns (II)”, the original author: yuan Runzi.

This paper focuses onStructural model(structural pattern), its main idea isAssemble multiple objects into a larger structure while maintaining the flexibility and efficiency of the structure, the coupling problem between modules is solved from the structure of the program.

Composite pattern

Structural patterns of 23 design patterns implemented in go language

sketch

In object-oriented programming, there are two common object design methods,combinationandinherit, both can solve the problem of code reuse, but when using the latter, it is prone to the side effects of too deep inheritance level and too complex object relationship, resulting in poor code maintainability. Therefore, a classic object-oriented design principle is:Combination is better than inheritance

As we all know, the semantics of combination is “has-a”, that is, the relationship between part and whole. The most classic combination mode is described as follows:

The objects are combined into a tree structure to represent the “part whole” hierarchy, so that users have consistency in the use of single objects and combined objects.

Go language naturally supports the combination mode, and from the characteristics that it does not support inheritance relationship, go also adheres to the principle that combination is better than inheritance, and encourages everyone to adopt the combination method in programming. There are two ways to implement the combination mode in go, namely direct composition and embedded composition. Let’s discuss these two different implementation methods together.

Go implementation

The implementation of direct composition is similar to Java / C + +, that is, one object is regarded as the member attribute of another object.

A typical implementation, such as the example given in 23 design patterns of GOF using go (I), is a message structure composed of header and body. Then the message is a whole, and the header and body are the components of the message.

type Message struct {
    Header *Header
    Body   *Body
}

Now let’s look at a slightly more complex example, also considering the plug-in architecture style message processing system described in the previous article. Previously, we solved the problem of plug-in loading with the abstract factory pattern. Usually, each plug-in has a life cycle. The common ones are start state and stop state. Now we use the composite pattern to solve the problem of plug-in start and stop.

First, add several life cycle related methods to the plugin interface:

package plugin
...
//Plug in running status
type Status uint8

const (
    Stopped Status = iota
    Started
)

type Plugin interface {
  //Launch plug-in
    Start()
  //Stop plug-in
    Stop()
  //Returns the current running state of the plug-in
    Status() Status
}
//The definitions of input, filter and output plug-in interfaces are similar to those in the previous article
//Here, the message structure is used instead of the original string to make the semantics clearer
type Input interface {
    Plugin
    Receive() *msg.Message
}

type Filter interface {
    Plugin
    Process(msg *msg.Message) *msg.Message
}

type Output interface {
    Plugin
    Send(msg *msg.Message)
}

For the plug-in message processing system, everything is a plug-in. Therefore, we also design pipeine as a plug-in to implement the plugin interface:

package pipeline
...
//A pipeline consists of three plugins: input, filter and output
type Pipeline struct {
    status plugin.Status
    input  plugin.Input
    filter plugin.Filter
    output plugin.Output
}

func (p *Pipeline) Exec() {
    msg := p.input.Receive()
    msg = p.filter.Process(msg)
    p.output.Send(msg)
}
//Start sequence output - > filter - > Input
func (p *Pipeline) Start() {
    p.output.Start()
    p.filter.Start()
    p.input.Start()
    p.status = plugin.Started
    fmt.Println("Hello input plugin started.")
}
//Stop sequence input - > filter - > output
func (p *Pipeline) Stop() {
    p.input.Stop()
    p.filter.Stop()
    p.output.Stop()
    p.status = plugin.Stopped
    fmt.Println("Hello input plugin stopped.")
}

func (p *Pipeline) Status() plugin.Status {
    return p.status
}

A pipeline is composed of input, filter and output plug-ins, forming a “part whole” relationship, and they all implement the plugin interface, which is the implementation of a typical combination mode. The client does not need to explicitly start and stop the input, filter and output plug-ins. When calling the start and stop methods of the pipeline object, pipeline has helped you start and stop the corresponding plug-ins in sequence.

Compared with the previous article, when implementing input, filter and output plug-ins in this article, you need to implement three more life cycles. Helloinput, upperfilter and consoleoutput in the previous article are also used as examples. The specific implementation is as follows:

package plugin
...
type HelloInput struct {
    status Status
}

func (h *HelloInput) Receive() *msg.Message {
  //If the plug-in is not started, nil is returned
    if h.status != Started {
        fmt.Println("Hello input plugin is not running, input nothing.")
        return nil
    }
    return msg.Builder().
        WithHeaderItem("content", "text").
        WithBodyItem("Hello World").
        Build()
}

func (h *HelloInput) Start() {
    h.status = Started
    fmt.Println("Hello input plugin started.")
}

func (h *HelloInput) Stop() {
    h.status = Stopped
    fmt.Println("Hello input plugin stopped.")
}

func (h *HelloInput) Status() Status {
    return h.status
}
package plugin
...
type UpperFilter struct {
    status Status
}

func (u *UpperFilter) Process(msg *msg.Message) *msg.Message {
    if u.status != Started {
        fmt.Println("Upper filter plugin is not running, filter nothing.")
        return msg
    }
    for i, val := range msg.Body.Items {
        msg.Body.Items[i] = strings.ToUpper(val)
    }
    return msg
}

func (u *UpperFilter) Start() {
    u.status = Started
    fmt.Println("Upper filter plugin started.")
}

func (u *UpperFilter) Stop() {
    u.status = Stopped
    fmt.Println("Upper filter plugin stopped.")
}

func (u *UpperFilter) Status() Status {
    return u.status
}

package plugin
...
type ConsoleOutput struct {
    status Status
}

func (c *ConsoleOutput) Send(msg *msg.Message) {
    if c.status != Started {
        fmt.Println("Console output is not running, output nothing.")
        return
    }
    fmt.Printf("Output:\n\tHeader:%+v, Body:%+v\n", msg.Header.Items, msg.Body.Items)
}

func (c *ConsoleOutput) Start() {
    c.status = Started
    fmt.Println("Console output plugin started.")
}

func (c *ConsoleOutput) Stop() {
    c.status = Stopped
    fmt.Println("Console output plugin stopped.")
}

func (c *ConsoleOutput) Status() Status {
    return c.status
}

The test code is as follows:

package test
...
func TestPipeline(t *testing.T) {
    p := pipeline.Of(pipeline.DefaultConfig())
    p.Start()
    p.Exec()
    p.Stop()
}
//Operation results
=== RUN   TestPipeline
Console output plugin started.
Upper filter plugin started.
Hello input plugin started.
Pipeline started.
Output:
    Header:map[content:text], Body:[HELLO WORLD]
Hello input plugin stopped.
Upper filter plugin stopped.
Console output plugin stopped.
Hello input plugin stopped.
--- PASS: TestPipeline (0.00s)
PASS

Another implementation of the composition pattern, embedded composition, actually uses the anonymous member feature of go language, which is essentially consistent with direct composition.

Take the message structure as an example. If embedded combination is adopted, it looks like this:

type Message struct {
    Header
    Body
}
//When using, message can refer to the member properties of header and body, for example:
msg := &Message{}
msg.SrcAddr = "192.168.0.1"

Adapter pattern

Structural patterns of 23 design patterns implemented in go language

sketch

The adapter pattern is one of the most commonly used structural patterns. It enables two objects that cannot work together because of interface mismatch to work together. In real life, the adapter mode can be seen everywhere, such as the power plug converter, which can make the British plug work on the Chinese socket. What the adapter pattern does isConvert an interface adapter into another interface target expected by the client through the adapterThe implementation principle is also simple, that is, Adapter realizes the Target interface and calls the Adaptee interface in the corresponding method.

A typical application scenario is that an old interface in the system is obsolete and will be abandoned. However, due to the historical burden, it is impossible to immediately replace all the old interfaces with new interfaces. At this time, an adapter can be added to adapt the old interface to a new interface for use. The adapter pattern well implements the open / closed principle in the object-oriented design principle. When adding an interface, there is no need to modify the old interface, just add an adaptation layer.

Go implementation

Continue to consider the example of the message processing system in the previous section. So far, the input of the system comes from helloinput. Now it is assumed that the function of receiving data from Kafka message queue needs to be added to the system. The interface of Kafka consumer is as follows:

package kafka
...
type Records struct {
    Items []string
}

type Consumer interface {
    Poll() Records
}

Because the current pipeline design is to receive data through the plugin.input interface, kafka.consumer cannot be directly integrated into the system.

What should I do? Use adapter mode!

To enable pipeline to use the kafka.consumer interface, we need to define an adapter as follows:

package plugin
...
type KafkaInput struct {
    status Status
    consumer kafka.Consumer
}

func (k *KafkaInput) Receive() *msg.Message {
    records := k.consumer.Poll()
    if k.status != Started {
        fmt.Println("Kafka input plugin is not running, input nothing.")
        return nil
    }
    return msg.Builder().
        WithHeaderItem("content", "text").
        WithBodyItems(records.Items).
        Build()
}

//Add Kafka into the input plug-in mapping relationship to create input objects through reflection
func init() {
    inputNames["hello"] = reflect.TypeOf(HelloInput{})
    inputNames["kafka"] = reflect.TypeOf(KafkaInput{})
}
...

Because the go language does not have a constructor, if you follow the instructions in the previous articleAbstract factory patternTo create kafkainput, the consumer member in the resulting instance will be nil because it has not been initialized. Therefore, we need to add a Init method to the Plugin interface to define some initialization operations of the plug-in and call it before the factory returns to the instance.

package plugin
...
type Plugin interface {
    Start()
    Stop()
    Status() Status
    // the new initialization method is called before the plug-in factory returns to the instance.
    Init()
}

//The modified plug-in factory is implemented as follows
func (i *InputFactory) Create(conf Config) Plugin {
    t, _ := inputNames[conf.Name]
    p := reflect.New(t).Interface().(Plugin)
  // return the plug-in instance before calling the Init function to complete the initialization method.
    p.Init()
    return p
}

//Implementation of init function of kakkainput
func (k *KafkaInput) Init() {
    k.consumer = &kafka.MockConsumer{}
}

Kafka. Mockconsumer in the above code is an implementation of our Kafka consumer model. The code is as follows:

package kafka
...
type MockConsumer struct {}

func (m *MockConsumer) Poll() *Records {
    records := &Records{}
    records.Items = append(records.Items, "i am mock consumer.")
    return records
}

The test code is as follows:

package test
...
func TestKafkaInputPipeline(t *testing.T) {
    config := pipeline.Config{
        Name: "pipeline2",
        Input: plugin.Config{
            PluginType: plugin.InputType,
            Name:       "kafka",
        },
        Filter: plugin.Config{
            PluginType: plugin.FilterType,
            Name:       "upper",
        },
        Output: plugin.Config{
            PluginType: plugin.OutputType,
            Name:       "console",
        },
    }
    p := pipeline.Of(config)
    p.Start()
    p.Exec()
    p.Stop()
}
//Operation results
=== RUN   TestKafkaInputPipeline
Console output plugin started.
Upper filter plugin started.
Kafka input plugin started.
Pipeline started.
Output:
    Header:map[content:kafka], Body:[I AM MOCK CONSUMER.]
Kafka input plugin stopped.
Upper filter plugin stopped.
Console output plugin stopped.
Pipeline stopped.
--- PASS: TestKafkaInputPipeline (0.00s)
PASS

Bridge pattern

Structural patterns of 23 design patterns implemented in go language

sketch

Bridge mode is mainly used forThe abstract part and the implementation part are decoupled so that they can change in an independent direction。 It solves the problem of class explosion caused by inheritance when the module has multiple change directions. For example, a product has two characteristics (change direction) of shape and color. The shape is divided into square and round, and the color is divided into red and blue. If the inherited design scheme is adopted, four product subcategories need to be added: square red, round red, square blue and round red. If there are m changes in shape and N changes in color, you need to add m * n product subcategories! Now we use the bridge mode for optimization, and design the shape and color as an abstract interface separately. In this way, we need to add two shape subclasses: square and circle, and two color subclasses: red and blue. Similarly, if there are m changes in shape and N changes in color, only m + n subclasses need to be added!
Structural patterns of 23 design patterns implemented in go language

In the above example, we abstract the shape and color into an interface, so that the product no longer depends on the specific shape and color details, so as to achieve the purpose of understanding the coupling.Bridging mode is essentially interface oriented programming, which can bring good flexibility and scalability to the system.If an object has multiple change directions, and each change direction needs to be expanded, it is most appropriate to use bridging mode for design.

Go implementation

Returning to the example of message processing system, a pipeline object is mainly composed of input, filter and output plug-ins (three features). Because it is a plug-in system, it is inevitable to support the implementation of multiple input, filter and output, and can be flexibly combined (with multiple changing directions). Obviously, pipeline is very suitable for designing with bridge mode, and we actually do the same. We design input, filter and output as an abstract interface respectively, and they extend in their respective directions. Pipeline only relies on these three abstract interfaces and does not perceive the details of the specific implementation.
Structural patterns of 23 design patterns implemented in go language

package plugin
...
type Input interface {
    Plugin
    Receive() *msg.Message
}

type Filter interface {
    Plugin
    Process(msg *msg.Message) *msg.Message
}

type Output interface {
    Plugin
    Send(msg *msg.Message)
}
package pipeline
...
//A pipeline consists of three plugins: input, filter and output
type Pipeline struct {
    status plugin.Status
    input  plugin.Input
    filter plugin.Filter
    output plugin.Output
}
//It is used through the abstract interface and can't see the implementation details of the bottom layer
func (p *Pipeline) Exec() {
    msg := p.input.Receive()
    msg = p.filter.Process(msg)
    p.output.Send(msg)
}

The test code is as follows:

package test
...
func TestPipeline(t *testing.T) {
    p := pipeline.Of(pipeline.DefaultConfig())
    p.Start()
    p.Exec()
    p.Stop()
}
//Operation results
=== RUN   TestPipeline
Console output plugin started.
Upper filter plugin started.
Hello input plugin started.
Pipeline started.
Output:
    Header:map[content:text], Body:[HELLO WORLD]
Hello input plugin stopped.
Upper filter plugin stopped.
Console output plugin stopped.
Pipeline stopped.
--- PASS: TestPipeline (0.00s)
PASS

summary

This paper mainly introduces the combination mode, adapter mode and bridge mode in structural mode. Composite pattern mainly solves the problem of code reuse. Compared with inheritance relationship, composite pattern can avoid the code complexity caused by too deep inheritance level. Therefore, the principle of combination over inheritance is popular in the field of object-oriented design, and the design of go language has also well practiced this principle; The adapter mode can be regarded as a bridge between two incompatible interfaces, which can convert one interface into another interface desired by the client, and solve the problem that modules cannot work together because of incompatible interfaces; The bridge pattern separates the abstract part and the implementation part of the module, so that they can expand in their respective directions, so as to achieve the purpose of decoupling.

Click focus to learn about Huawei cloud’s new technologies for the first time~