preface

The previous article “23 Design Patterns for GoF with Go (I)” introduces the Creational Pattern among the 23 design patterns. The Creational Pattern is a kind of design Pattern that deals with object creation. The main idea is to hide the details of object creation from the users of objects, so as to achieve the purpose of decoupling. This paper mainly focuses on Structural Pattern, 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. There are two ways to realize the combination mode of Go, namely Direct Composition and Embedding Composition. Let’s discuss these two different implementation methods together.

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
...
// Plug-in running status
type Status uint8

const (
	Stopped Status = iota
	Started
)

type Plugin interface {
  // Start the plug-in
	Start()
  // Stop the plugin
	Stop()
  // Returns the current running status of the plug-in
	Status() Status
}
// The Input, Filter, and Output plug-in interfaces are defined similarly to those in the previous article
// The Message structure is used instead of 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)
}

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
...
// A Pipeline consists of input, filter, output Plugin
type Pipeline struct {
	status plugin.Status
	input  plugin.Input
	filter plugin.Filter
	output plugin.Output
}

func (p *Pipeline) Exec(a) {
	msg := p.input.Receive()
	msg = p.filter.Process(msg)
	p.output.Send(msg)
}
Output -> filter -> input
func (p *Pipeline) Start(a) {
	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(a) {
	p.input.Stop()
	p.filter.Stop()
	p.output.Stop()
	p.status = plugin.Stopped
	fmt.Println("Hello input plugin stopped.")}func (p *Pipeline) Status(a) 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(a) *msg.Message {
  // If the plug-in is not started, nil is returned
	ifh.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(a) {
	h.status = Started
	fmt.Println("Hello input plugin started.")}func (h *HelloInput) Stop(a) {
	h.status = Stopped
	fmt.Println("Hello input plugin stopped.")}func (h *HelloInput) Status(a) Status {
	return h.status
}
Copy the code
package plugin
...
type UpperFilter struct {
	status Status
}

func (u *UpperFilter) Process(msg *msg.Message) *msg.Message {
	ifu.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(a) {
	u.status = Started
	fmt.Println("Upper filter plugin started.")}func (u *UpperFilter) Stop(a) {
	u.status = Stopped
	fmt.Println("Upper filter plugin stopped.")}func (u *UpperFilter) Status(a) Status {
	return u.status
}

Copy the code
package plugin
...
type ConsoleOutput struct {
	status Status
}

func (c *ConsoleOutput) Send(msg *msg.Message) {
	ifc.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(a) {
	c.status = Started
	fmt.Println("Console output plugin started.")}func (c *ConsoleOutput) Stop(a) {
	c.status = Stopped
	fmt.Println("Console output plugin stopped.")}func (c *ConsoleOutput) Status(a) 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.Stop()
}
// Run the result
=== 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
Copy the code

Another realization of Composition mode, Embedding Composition, actually uses the anonymous member feature of Go language, which is essentially consistent with direct Composition.

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

type Message struct {
	Header
	Body
}
// When used, Message can refer to the Header and Body member attributes, for example:
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 is to do an interface Adaptee, through the Adapter Adapter into the Client expected another interface Target to use, the implementation principle is also very simple, is Adapter through the Target interface, and in the corresponding method to call Adaptee interface implementation.

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(a) *msg.Message {
	records := k.consumer.Poll()
	ifk.status ! = Started { fmt.Println("Kafka input plugin is not running, input nothing.")
		return nil
	}
	return msg.Builder().
		WithHeaderItem("content"."kafka").
		WithBodyItems(records.Items).
		Build()
}

// Add kafka to the input plug-in mapping to create an input object through reflection
func init(a) {
	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
	// Add an initialization method called before the plug-in factory returns 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)
  // Call Init before returning the plug-in instance to complete the relevant initialization methods
	p.Init()
	return p
}

// KakkaInput Init function implementation
func (k *KafkaInput) Init(a) {
	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(a) *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.Exec()
	p.Stop()
}
// Run the result
=== 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
Copy 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)
}
Copy the code
package pipeline
...
// A Pipeline consists of input, filter, output Plugin
type Pipeline struct {
	status plugin.Status
	input  plugin.Input
	filter plugin.Filter
	output plugin.Output
}
// Use the abstract interface without seeing the underlying implementation details
func (p *Pipeline) Exec(a) {
	msg := p.input.Receive()
	msg = p.filter.Process(msg)
	p.output.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.Stop()
}
// Run the result
=== 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
Copy 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.

In the next article, we will continue to focus on structural patterns, covering the remaining four patterns: decorator, facade, meta, and proxy.