Abstract: This article focuses on the structural pattern (Structural Pattern), the main idea is to assemble multiple objects into a larger structure, while maintaining the flexibility and efficiency of the structure, from the structure of the program to solve the problem between modules The coupling problem.

This article is shared from HUAWEI CLOUD COMMUNITY " coming soon, here are the Go language implementations of 23 design patterns (2) ", the original author: Yuan Runzi.

This article focuses on the structural pattern (Structural Pattern). The main idea is that assembles multiple objects into a larger structure, while maintaining the flexibility and efficiency , which solves the problem from the structure of the program. The coupling problem.

Composite Pattern

image.png

Brief introduction

In object-oriented programming, there are two common object design methods, combination and inherit , both of which can solve the problem of code reuse, but when the latter is used, it is easy to have too deep inheritance levels and too complicated object relationships As a side effect, the maintainability of the code deteriorates. Therefore, a classic object-oriented design principle is: combination is better than inheritance .

We all know that the semantics of combination is "has-a", which is the relationship between part and whole. The most classic combination mode is described as follows:

Combine objects into a tree structure to represent a "part-whole" hierarchical structure, so that users have consistency in the use of single objects and combined objects.

The Go language naturally supports the composition mode, and from the point of view of its lack of support for inheritance relationships, Go also pursues the principle of composition over inheritance, encouraging everyone to use composition methods when designing programs. There are two ways to implement the composition mode in Go, namely Direct Composition and Embedding 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 used as a member attribute of another object.

A typical implementation is the example in "23 Design Patterns of GoF Using Go (1)". A Message structure is composed of Header and Body. Then Message is a whole, and Header and Body are components of the message.

type Message struct {
    Header *Header
    Body   *Body
}

Now, let's look at a slightly more complicated example, also consider the plug-in architecture style message processing system described in the previous article. Earlier, we used the abstract factory model to solve the problem of plug-in loading. Usually, each plug-in has a life cycle. The common ones are the startup state and the stop state. Now we use the combined mode to solve the problem of plug-in startup and stop.

First, add several lifecycle-related methods to the Plugin interface:

package plugin
...
// 插件运行状态
type Status uint8

const (
    Stopped Status = iota
    Started
)

type Plugin interface {
  // 启动插件
    Start()
  // 停止插件
    Stop()
  // 返回插件当前的运行状态
    Status() Status
}
// Input、Filter、Output三类插件接口的定义跟上一篇文章类似
// 这里使用Message结构体替代了原来的string,使得语义更清晰
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 a plug-in message processing system, everything is a plug-in, so we also design Pipeine as a plug-in to implement the Plugin interface:

package pipeline
...
// 一个Pipeline由input、filter、output三个Plugin组成
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)
}
// 启动的顺序 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.")
}
// 停止的顺序 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 three types of plug-ins: Input, Filter, and Output, forming a "part-to-whole" relationship, and they all implement the Plugin interface, which is the realization of a typical combination mode. Client does not need to explicitly start and stop Input, Filter, and Output plug-ins. When calling the Start and Stop methods of the Pipeline object, Pipeline has already helped you to start and stop the corresponding plug-ins in order.

Compared with the previous article, when implementing the Input, Filter, and Output three types of plug-ins in this article, three more life cycle methods need to be implemented. Let’s take the HelloInput, UpperFilter, and ConsoleOutput in the previous article as examples. The specific implementation is as follows:

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

func (h *HelloInput) Receive() *msg.Message {
  // 如果插件未启动,则返回nil
    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()
}
// 运行结果
=== 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 model, Embedding Composition, actually uses the anonymous member feature of the Go language, which is essentially the same as direct composition.

Taking the Message structure as an example, if an embedded combination is used, it looks like this:

type Message struct {
    Header
    Body
}
// 使用时,Message可以引用Header和Body的成员属性,例如:
msg := &Message{}
msg.SrcAddr = "192.168.0.1"

Adapter Pattern

image.png

Brief introduction

The adapter pattern is one of the most commonly used structural patterns. It allows two objects that cannot work together because of interface mismatches to work together. In real life, the adapter mode is also seen everywhere, such as a power plug converter, which allows British plugs to work on Chinese sockets. What the adapter mode does is converts an interface Adaptee through the adapter Adapter into another interface Target expected by the Client to use . The implementation principle is also very simple, that is, Adapter implements the Target interface and calls Adaptee in the corresponding method. Implementation of the interface.

A typical application scenario is that an old interface in the system is outdated and will be obsolete, but because of the historical burden, it is impossible to immediately replace the old interface with the new interface. At this time, you can add an adapter to adapt the old interface to the new one. Interface to use. The adapter mode implements the open/closed principle in the object-oriented design principles. When adding an interface, there is no need to modify the old interface, just add an additional 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 is derived from HelloInput. Now suppose that it is necessary to add the function of receiving data from the Kafka message queue to the system. The interface of the Kafka consumer is as follows:

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

type Consumer interface {
    Poll() Records
}

Since the current pipeline design is to receive data through the plugin.Input interface, kafka.Consumer cannot be directly integrated into the system.

How to do? Use adapter mode!

In order 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()
}

// 在输入插件映射关系中加入kafka,用于通过反射创建input对象
func init() {
    inputNames["hello"] = reflect.TypeOf(HelloInput{})
    inputNames["kafka"] = reflect.TypeOf(KafkaInput{})
}
...

Because Go language does not have a constructor, if abstract factory pattern 160d950517163a in the previous article, the consumer member in the obtained instance will be nil because it has not been initialized. Therefore, it is necessary to add an Init method to the Plugin interface to define some initialization operations of the plug-in, and to call it before the factory returns to the instance.

package plugin
...
type Plugin interface {
    Start()
    Stop()
    Status() Status
    // 新增初始化方法,在插件工厂返回实例前调用
    Init()
}

// 修改后的插件工厂实现如下
func (i *InputFactory) Create(conf Config) Plugin {
    t, _ := inputNames[conf.Name]
    p := reflect.New(t).Interface().(Plugin)
  // 返回插件实例前调用Init函数,完成相关初始化方法
    p.Init()
    return p
}

// KakkaInput的Init函数实现
func (k *KafkaInput) Init() {
    k.consumer = &kafka.MockConsumer{}
}

The kafka.MockConsumer in the above code is an implementation of our model Kafka consumer, 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()
}
// 运行结果
=== 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

image.png

Brief introduction

The bridge mode is mainly used for to decouple the abstract part and the realization part, so that they can change to independent directions. It solves the problem of class explosion caused by inheritance when the module has multiple changing directions. For example, a product has two characteristics (change directions) of shape and color, where the shape is divided into square and round, and the color is divided into red and blue. If the inherited design scheme is adopted, then 4 new product sub-categories need to be added: square red, round red, square blue, and round red. If there are a total of m changes in shape and n changes in color, then m*n product subcategories need to be added! Now we use the bridge mode to optimize, design the shape and color as an abstract interface separately, so we need to add two shape subtypes: square and circle, and two color subtypes: red and blue. Similarly, if there are a total of m changes in shape and n changes in color, only m+n subcategories need to be added in total!
image.png

In the above example, we abstract the shape and color as an interface, so that the product no longer depends on the specific shape and color details, so as to achieve the purpose of decoupling. bridge mode is essentially interface-oriented programming, which can bring great flexibility and scalability to the system. If there are multiple directions of change for an object, and each direction of change needs to be expanded, then it is more appropriate to use the bridge mode for design.

Go implementation

Going back to the example of the message processing system, a Pipeline object is mainly composed of three types of plug-ins: Input, Filter, and Output (3 features). Because it is a plug-in system, it is inevitably required to support multiple implementations of Input, Filter, and Output. , And can be combined flexibly (with multiple directions of change). Obviously, Pipeline is very suitable for designing using the bridge mode, and in fact we did the same. We design Input, Filter, and Output as an abstract interface, and they expand in their respective directions. Pipeline only relies on these three abstract interfaces, and does not perceive the details of the specific implementation.
image.png

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
...
// 一个Pipeline由input、filter、output三个Plugin组成
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)
}

The test code is as follows:

package test
...
func TestPipeline(t *testing.T) {
    p := pipeline.Of(pipeline.DefaultConfig())
    p.Start()
    p.Exec()
    p.Stop()
}
// 运行结果
=== 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

to sum up

This article mainly introduces the combination mode, adapter mode and bridge mode in the structural mode. The composition mode mainly solves the problem of code reuse. Compared with the inheritance relationship, the composition mode can avoid the complexity of the code caused by the deep inheritance level. Therefore, the principle of composition is better than inheritance in the field of object-oriented design, and the design of the Go language is also This principle is well practiced; the adapter mode can be seen as a bridge between two incompatible interfaces. One interface can be converted into another interface that Client wants, which solves the problem that modules cannot work together due to incompatible interfaces. The problem; the bridge mode separates the abstract part and the realization part of the module, so that they can expand in their respective directions, so as to achieve the purpose of decoupling.

Click to follow and learn about Huawei Cloud's fresh technology for the first time~


华为云开发者联盟
1.4k 声望1.8k 粉丝

生于云,长于云,让开发者成为决定性力量