Abstract: This paper mainly focuses on Structural Pattern, its main idea is to assemble multiple objects into a larger structure, and at the same time maintain the flexibility and efficiency of the structure, from the program structure to solve the coupling problem between modules.

This article is shared by Huawei Cloud community “Come, here are 23 design modes of Go language implementation (2)”, the original author: Yuan Runzi.

This paper mainly focuses on StructuralPattern, whose main idea is to assemble multiple objects into a larger structure, while maintaining the flexibility and efficiency of the structure, and solve the coupling problem between modules from the structure of the program.

Composite Pattern

The paper

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

As we all know, the semantic meaning of combination is “has-A”, that is, the relationship between part and whole. The most classical combination mode is described as follows:

Objects are grouped into a tree structure to represent a partial-whole hierarchy, allowing consistency in the use of individual objects and composite objects.

The Go language naturally supports the composition pattern, and because it does not support inheritance, it also upholds the principle of composition over inheritance, encouraging the use of composition in programming. Go implements composition patterns in two ways, DirectComposition and embedded composition, which we’ll explore in the following sections.

Go to realize

Direct Composition is implemented in a similar way to Java/C++, with one object as a member property of another.

A typical implementation, such as the example in 23 Design Patterns for GoF with Go (Part 1), is a Message structure consisting of a Header and a Body. The Message is the whole thing, and the Header and Body are the parts of the Message.

type Message struct {
	Header *Header
	Body   *Body
}
Copy the code

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

Start by adding several life-cycle related methods to the Plugin interface:

package plugin ... // Uint8 const (Stopped Status = iota Started) type Plugin interface {// Start() // Stop() // return the current running Status of the plugin. Status() Status} 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) }Copy the code

For a plug-in message processing system, everything is a plug-in, so we designed Pipeine as a plug-in, implementing the Plugin interface:

package 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) Output -> filter -> input func (p *Pipeline) Start() {p.utput.start () p.filter.start () 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 }Copy the code

A Pipeline is composed of Input, Filter and Output plug-ins, forming a “part-whole” relationship, and they all realize the Plugin interface, which is a typical combination mode implementation. The Client does not need to explicitly Start and Stop the Input, Filter, and Output plugins. Pipeline starts and stops them for you in sequence when it calls the Start and Stop methods on the Pipeline object.

Compared with the previous article, three more life cycle methods are needed to implement the Input, Filter and Output plug-ins in this paper. HelloInput, UpperFilter and ConsoleOutput as examples from the previous article are implemented as follows:

package plugin ... Type HelloInput struct {status status} func (h *HelloInput) Receive() * msg.message {// If the plugin is not started, return 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 }Copy the code

The test code is as follows:

package test ... Func TestPipeline(t * testing.t) {p := pipeline.of (pipeline.defaultConfig ()) p.start () p.exec () p.top () 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) PASSCopy the code

Another implementation of the composition pattern, embedded composition, takes advantage of the anonymous member nature of the Go language and is essentially the same as direct composition.

Using the Message structure as an example, if you use an embedded composition, it would look like this:

MSG := &Message{} msg.srcaddr = "192.168.0.1"Copy the code

Adapter Pattern

The paper

The adapter pattern is one of the most commonly used structural patterns, enabling two objects to work together that would otherwise not work together because of mismatched interfaces. In real life, adapter patterns are everywhere, such as power plug converters, which allow British plugs to work in Chinese sockets. Adapter mode to do is to an interface Adaptee, through the Adapter Adapter into the Client expected another interface Target to use, the implementation principle is also very simple, is that Adapter by implementing Target interface, And invoke the Adaptee interface implementation in the corresponding method.

A typical application scenario is that an old interface in the system is outdated and about to be discarded. However, due to the historical burden, the old interface cannot be replaced with the new one immediately. In this case, you can add an adapter to adapt the old interface to the new one. The adapter pattern is a good implementation of the open/ Closed principle of object-oriented design. When you add an interface, you don’t need to modify the old interface, just add an adaptation layer.

Go to realize

Continuing with the message processing system example from the previous section, so far the input to the system has come from HelloInput. Now suppose we need to add the ability to receive data from a Kafka message queue, where the Kafka consumer interface looks like this:

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


type Consumer interface {
	Poll() Records
}
Copy the code

Kafka.consumer is not directly integrated into the system because the current Pipeline design is to receive data through the plugin.input interface.

How to do? Use adapter mode! To enable a 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 (record.items).build ()} // Add kafka to the input plug-in mapping, Func init() {inputNames["hello"] = reflect.typeof (HelloInput{}) inputNames["kafka"] = reflect.TypeOf(KafkaInput{}) } ...Copy the code

Because the Go language has no constructor, if KafkaInput was created following the abstract factory pattern in the previous article, the resulting consumer member in the instance would be nil because it was not initialized. Therefore, we need to add an Init method to the Plugin interface that defines some of the plug-in’s initialization operations and calls them before the factory returns an instance.

package plugin ... Type Plugin interface {Start() Stop() Status() Status Func (I *InputFactory) Create(conf Config) Plugin {t, _ := inputNames[conf.name] p := reflect.new (t).interface ().(Plugin) Func (k *KafkaInput) Init() {k.consumer = &kafka.mockConsumer {}}Copy the code

The MockConsumer in the above code is an implementation of our pattern Kafka consumer 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
}
Copy the code

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.ec () p.top ()} // 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) PASSCopy the code

Bridge Pattern

The paper

The bridge pattern is mainly used to decouple the abstract part from the implementation part so that they can vary in independent directions. It solves the problem of class explosion caused by inheritance when modules have multiple directions of change. For example, a product has two characteristics (direction of change), shape is divided into square and round, color is divided into red and blue. If you use the inherited design, you need to add four new product subclasses: 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 you need m* N new product subclasses! Now let’s use the bridge mode to optimize and separate the shape and color into an abstract interface, which requires two new shape subclasses: square and circle, and two new color subclasses: red and blue. Similarly, if there are a total of M changes in shape and n changes in color, there are only m+ N new subclasses in total!

In the example above, we achieve decoupling by abstracting shape and color as an interface, so that the product is no longer dependent on concrete shape and color details. Bridge mode is interface oriented programming in essence, which can bring good flexibility and scalability to the system. If an object has multiple directions of change, and each direction of change needs to be extended, then the bridging pattern is the most appropriate way to design.

Go to realize

Back to the example of message processing system, a Pipeline object is mainly composed of Input, Filter and Output plug-ins (three characteristics). As a plug-in system, it is inevitable to support the implementation of multiple Input, Filter and Output. And can be flexibly combined (there are multiple directions of change). Obviously, pipelines are perfect for bridging, and we do. Input, Filter, and Output are designed as abstract interfaces, which expand in their respective directions. Pipeline only relies on these three abstract interfaces and is not aware of implementation details.

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 ... Struct {status plugin.status input plugin.input filter Plugin.filter output plugin.output} Func (p *Pipeline) Exec() {MSG := p.infute.receive () MSG = p.filter.process (MSG) p.utput.send (MSG)}Copy the code

The test code is as follows:

package test ... Func TestPipeline(t * testing.t) {p := pipeline.of (pipeline.defaultConfig ()) p.start () p.exec () p.top () 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) PASSCopy the code

conclusion

This paper mainly introduces the composite pattern, adapter pattern and bridge pattern of structural pattern. Combined pattern mainly solves the problem of code reuse. Compared with inheritance relationship, combined pattern can avoid the code complexity caused by too deep inheritance level. Therefore, the principle of combination is better than inheritance is spread in the field of object-oriented design, and the design of Go language also well practices this principle. Adapter mode can be regarded as a bridge between two incompatible interfaces. It can convert one interface into another interface desired by the Client, which solves the problem that modules cannot work together because of interface incompatibility. The bridge pattern decouples the abstract and implementation parts of a module, allowing them to expand in their respective directions.

Click to follow, the first time to learn about Huawei cloud fresh technology ~