preface

Twenty-five years have passed since GoF proposed 23 design patterns in 1995, and design patterns are still a hot topic in software. Nowadays, if you don’t know any design patterns, you are embarrassed to call yourself a qualified programmer. Design patterns are generally defined as:

Design Pattern is a set of repeatedly used, most people know, classified, summarized code Design experience, the use of Design Pattern in order to reuse the code, make the code easier to understand and ensure the reliability of the code.

By definition, a design pattern is a summary of experience, a concise and elegant solution to a particular problem. The immediate benefit of learning design patterns is that you can stand on the shoulders of giants to solve specific problems in software development. However, the highest level of learning design patterns is to learn the ideas that are used to solve problems. Once you know their essence, you will be able to solve a particular problem even if you have forgotten the name and structure of a design pattern.

Good things are touted, of course, will be blackened. Design patterns have been attacked for two reasons:

1. Design patterns increase code volume and complicate program logic. This is inevitable, but we can’t just consider development costs. The simplest program is, of course, a function written from beginning to end, but such late maintenance costs can become very large; Design patterns add a bit of development cost, but they allow people to write reusable, maintainable programs. To quote the philosophy of Software Design, the former is tactical programming, the latter is strategic programming, and we should Say No to tactical programming! (See Step by Step Reducing Software Complexity)

Abuse of design patterns. This is the most common mistake beginners make. When they learn a pattern, they want to use it in all the code, so they deliberately use the pattern where they should not be used, resulting in extremely complex programs. There are several key elements to every design pattern: scenarios, solutions, and strengths and weaknesses. Patterns are not a panacea. They only work for specific problems. So, before using a pattern, ask yourself, does the current scenario apply to this pattern?

The subtitle of Design Patterns is “Fundamentals of Reusable Object-oriented Software,” but that doesn’t mean that only object-oriented languages can use design patterns. A pattern is just an idea for solving a particular problem. It has nothing to do with language. Like Go, it is not an object-oriented language like C++ and Java, but design patterns apply as well. This series of articles will use Go language to realize 23 design patterns proposed by GoF, which are organized into three categories: Creational Pattern, Structural Pattern and Behavioral Pattern. The text focuses on the creation pattern.

Singleton Pattern

The paper

The singleton pattern, the simplest of the 23 design patterns, is used to ensure that a class has only one instance and to provide a global access point to it.

In programming, there are some objects that we usually only need a shared instance of, such as thread pool, global cache, object pool, etc., which is suitable for the singleton pattern.

However, not all globally unique scenarios are suitable for using the singleton pattern. For example, consider that you need to count an API call with two metrics, the number of successful calls and the number of failed calls. Both metrics are globally unique, so one might model them as two singleton SuccessapiMetrics and failapimetrics. Along these lines, as the number of metrics increases, you’ll find that your code will become more and more defined and bloated with classes. This is also the most common misapplication scenario for the singleton pattern. A better approach would be to design two metrics into two instances of ApiMetric success and ApiMetic Fail under one object.

How do you determine whether an object should be modeled as a singleton?

In general, objects modeled as singletons have a “central point” meaning, such as a thread pool, which is the center that manages all threads. So, before deciding whether an object fits the singleton pattern, ask yourself, is this object a central point?

Go to realize

When implementing the singleton pattern on an object, there are two things you must be careful about :(1) restricting the caller from instantiating the object directly; (2) Provide a globally unique access method for singletons of this object.

For C++/Java, you simply make the constructor of a class private and provide a static method to access a unique instance of the class. But with the Go language, there is no concept of constructors and no static methods, so you need to look elsewhere.

We can implement this by using the access rules of package in Go language. By making the singleton lowercase, we can restrict its access scope to the current package, emulating the private constructors in C++/Java. Implementing an accessor function that starts with a capital letter under the current package is equivalent to the static method.

In real development, we often encounter objects that need to be created and destroyed frequently. The frequent creation and destruction of items consumes CPU and memory, and is usually optimized using object pooling techniques. Consider that we need to implement a Message object pool, which is the global central point to manage all Message instances, so we will actually have a singleton as follows:

package msgpool
...
/ / the message pool
type messagePool struct {
	pool *sync.Pool
}
// Message pool singleton
var msgPool = &messagePool{
	// If there are no messages in the Message pool, create a new Message instance with Count 0
	pool: &sync.Pool{New: func(a) interface{} { return &Message{Count: 0}}}},// The only way to access the message pool singleton
func Instance(a) *messagePool {
	return msgPool
}
// Add messages to the message pool
func (m *messagePool) AddMsg(msg *Message) {
	m.pool.Put(msg)
}
// Get messages from the message pool
func (m *messagePool) GetMsg(a) *Message {
	return m.pool.Get().(*Message)
}
...
Copy the code

The test code is as follows:

package test
...
func TestMessagePool(t *testing.T) {
	msg0 := msgpool.Instance().GetMsg()
	ifmsg0.Count ! =0 {
		t.Errorf("expect msg count %d, but actual %d.".0, msg0.Count)
	}
	msg0.Count = 1
	msgpool.Instance().AddMsg(msg0)
	msg1 := msgpool.Instance().GetMsg()
	ifmsg1.Count ! =1 {
		t.Errorf("expect msg count %d, but actual %d.".1, msg1.Count)
	}
}
// Run the result
=== RUN   TestMessagePool
--- PASS: TestMessagePool (0.00s)
PASS
Copy the code

The above singleton pattern is a typical “hungry man pattern” in which instances are initialized when the system loads. Correspondingly, there is a “lazy mode” in which an object is not initialized until it is in use, thereby saving some memory. It is well known that “lazy mode” causes thread-safety problems and can be optimized with plain locking or, more efficiently, double-checked locking. The Go language has a more elegant implementation of the lazy mode using sync.once, which has a Do method whose input parameter is a method that the Go language guarantees to be called only Once.

// A lazy implementation of the singleton pattern
package msgpool
...
var once = &sync.Once{}
// Message pool singleton, initialized on first invocation
var msgPool *messagePool
// globally uniquely get the message pool to the method
func Instance(a) *messagePool {
	// Implement the initialization logic in anonymous functions. Go guarantees that it will only be called once
	once.Do(func(a) {
		msgPool = &messagePool{
			// If there are no messages in the Message pool, create a new Message instance with Count 0
			pool: &sync.Pool{New: func(a) interface{} { return &Message{Count: 0}}}}})return msgPool
}
...
Copy the code

Builder Pattern

The paper

In the program design, we often encounter some complex objects, which have many member attributes, or even nested multiple complex objects. In this case, creating this complex object becomes tedious. For C++/Java, the most common manifestation is a constructor with a long argument list:

MyObject obj = new MyObject(param1, param2, param3, param4, param5, param6, ...)
Copy the code

The most common manifestation of the Go language is nested instantiation of multiple layers:

obj := &MyObject{
  Field1: &Field1 {
    Param1: &Param1 {
      Val: 0,
    },
    Param2: &Param2 {
      Val: 1,},... }, Field2: &Field2 { Param3: &Param3 { Val:2,},... },... }Copy the code

The above object creation method has two obvious disadvantages :(1) it is not friendly to object users, and users need to know too much detail when creating objects; (2) The code readability is poor.

For such scenarios where there are many object members and the object creation logic is cumbersome, it is suitable to use the Builder pattern for optimization.

The role of the builder model is as follows:

1. Encapsulate the creation process of complex objects, so that object users do not perceive the complex creation logic.

2. You can assign values to members step by step, or create nested objects, and finally complete the creation of the target object.

3. Reuse the same object creation logic for multiple objects.

Among them, the first and second points are more commonly used, and the implementation of the builder pattern is mainly based on these two points.

Go to realize

Consider the following Message structure, consisting of a Header and a Body:

package msg
...
type Message struct {
	Header *Header
	Body   *Body
}
type Header struct {
	SrcAddr  string
	SrcPort  uint64
	DestAddr string
	DestPort uint64
	Items    map[string]string
}
type Body struct {
	Items []string}...Copy the code

If you follow the direct object creation approach, the creation logic would look like this:

// Multi-layer nested instantiation
message := msg.Message{
	Header: &msg.Header{
		SrcAddr:  "192.168.0.1",
		SrcPort:  1234,
		DestAddr: "192.168.0.2",
		DestPort: 8080,
		Items:    make(map[string]string),
	},
	Body:   &msg.Body{
		Items: make([]string.0),}}// You need to know the implementation details of the object
message.Header.Items["contents"] = "application/json"
message.Body.Items = append(message.Body.Items, "record1")
message.Body.Items = append(message.Body.Items, "record2")
Copy the code

Although the Message structure does not have many levels of nesting, it does have the disadvantage of being user-unfriendly and poorly readable from the perspective of the code it creates. Let’s introduce the Builder pattern to refactor the code:

package msg
...
// Message object Builder object
type builder struct {
	once *sync.Once
	msg *Message
}
// Return the Builder object
func Builder(a) *builder {
	return &builder{
		once: &sync.Once{},
		msg: &Message{Header: &Header{}, Body: &Body{}},
	}
}
// Here are the build methods for the Message members
func (b *builder) WithSrcAddr(srcAddr string) *builder {
	b.msg.Header.SrcAddr = srcAddr
	return b
}
func (b *builder) WithSrcPort(srcPort uint64) *builder {
	b.msg.Header.SrcPort = srcPort
	return b
}
func (b *builder) WithDestAddr(destAddr string) *builder {
	b.msg.Header.DestAddr = destAddr
	return b
}
func (b *builder) WithDestPort(destPort uint64) *builder {
	b.msg.Header.DestPort = destPort
	return b
}
func (b *builder) WithHeaderItem(key, value string) *builder {
  // Ensure that the map is initialized only once
	b.once.Do(func(a) {
		b.msg.Header.Items = make(map[string]string)
	})
	b.msg.Header.Items[key] = value
	return b
}
func (b *builder) WithBodyItem(record string) *builder {
	b.msg.Body.Items = append(b.msg.Body.Items, record)
	return b
}
// Create the Message object, called in the last step
func (b *builder) Build(a) *Message {
	return b.msg
}
Copy the code

The test code is as follows:

package test
...
func TestMessageBuilder(t *testing.T) {
  // Use message builder for object creation
	message := msg.Builder().
		WithSrcAddr("192.168.0.1").
		WithSrcPort(1234).
		WithDestAddr("192.168.0.2").
		WithDestPort(8080).
		WithHeaderItem("contents"."application/json").
		WithBodyItem("record1").
		WithBodyItem("record2").
		Build()
	ifmessage.Header.SrcAddr ! ="192.168.0.1" {
		t.Errorf("expect src address 192.168.0.1, but actual %s.", message.Header.SrcAddr)
	}
	if message.Body.Items[0] != "record1" {
		t.Errorf("expect body item0 record1, but actual %s.", message.Body.Items[0])}}// Run the result
=== RUN   TestMessageBuilder
--- PASS: TestMessageBuilder (0.00s)
PASS
Copy the code

The test code shows that using the Builder pattern for object creation eliminates the need for users to know the implementation details of the object and makes the code more readable.

Factory Method Pattern

The paper

The factory method pattern is similar to the builder pattern discussed in the previous section in that it encapsulates the logic of object creation and provides consumers with an easy-to-use interface for object creation. The two are slightly different in application scenarios, in that the Builder pattern is more commonly used in scenarios where multiple parameters are passed for instantiation.

Using the factory approach to create objects has two main benefits:

1. Better code readability. The factory method is much more readable because it can express the meaning of the code through function names than using constructors in C++/Java or {} in Go to create objects. For example, creating a productA object using the factory method productA := CreateProductA() is more readable than using productA := productA {} directly.

2. Decouple from consumer code. In many cases, the creation of an object is a volatile point, and encapsulating the creation of an object with a factory approach avoids shotgun changes when creating logical changes.

The factory method pattern is also implemented in two ways :(1) provide a factory object and create a product object by calling the factory method of the factory object; (2) integrate factory methods into product objects (static methods of objects in C++/Java, functions under the same package in Go)

Go to realize

Consider an Event object with two valid time types Start and End:

package event
...
type Type uint8
// Event type definition
const (
	Start Type = iota
	End
)
// Event abstraction interface
type Event interface {
	EventType() Type
	Content() string
}
// Start the Event, which implements the Event interface
type StartEvent struct{
	content string}...// The Event interface is implemented
type EndEvent struct{
	content string}...Copy the code

1, according to the first implementation, provide a factory object for the Event, the code is as follows:

package event
...
// Event factory object
type Factory struct{}
// Create a specific event with the event type
func (e *Factory) Create(etype Type) Event {
	switch etype {
	case Start:
		return &StartEvent{
			content: "this is start event",}case End:
		return &EndEvent{
			content: "this is end event",}default:
		return nil}}Copy the code

The test code is as follows:

package test
...
func TestEventFactory(t *testing.T) {
	factory := event.Factory{}
	e := factory.Create(event.Start)
	ife.EventType() ! = event.Start { t.Errorf("expect event.Start, but actual %v.", e.EventType())
	}
	e = factory.Create(event.End)
	ife.EventType() ! = event.End { t.Errorf("expect event.End, but actual %v.", e.EventType())
	}
}
// Run the result
=== RUN   TestEventFactory
--- PASS: TestEventFactory (0.00s)
PASS
Copy the code

2. According to the second implementation method, a separate factory method is provided for the Start and End events respectively, with the following code:

package event
...
// Factory method of type Start Event
func OfStart(a) Event {
	return &StartEvent{
		content: "this is start event",}}// Factory method of type End Event
func OfEnd(a) Event {
	return &EndEvent{
		content: "this is end event",}}Copy the code

The test code is as follows:

package event
...
func TestEvent(t *testing.T) {
	e := event.OfStart()
	ife.EventType() ! = event.Start { t.Errorf("expect event.Start, but actual %v.", e.EventType())
	}
	e = event.OfEnd()
	ife.EventType() ! = event.End { t.Errorf("expect event.End, but actual %v.", e.EventType())
	}
}
// Run the result
=== RUN   TestEvent
--- PASS: TestEvent (0.00s)
PASS
Copy the code

Abstract Factory Pattern

The paper

In the factory method pattern, we use a factory object to create a product family. The specific product is determined by the Swtich-case method. This also means that for each new class of product objects added to the product group, the original factory object code must be modified; And with the increasing of products, the responsibility of factory objects is becoming heavier and heavier, which violates the principle of single responsibility.

The Abstract Factory pattern solves this problem by adding an abstraction layer to the factory class. As shown in the figure above, FactoryA and FactoryB both implement the abstract Factory interface to create ProductA and ProductB, respectively. If ProductC is added later, you only need to add a FactoryC without modifying the original code. Because each factory is responsible for creating only one product, the single responsibility principle is followed.

Go to realize

Consider a message processing system that requires the following plug-in architecture style: pipeline is a message processing pipeline that contains input, filter, and Output plug-ins. We need the implementation to create pipelines according to the configuration. The implementation of the plug-in loading process is very suitable to use the factory pattern. The abstract factory pattern is used for the creation of input, Filter, and Output plug-ins, while the factory method pattern is used for the creation of pipelines.

The interfaces of various plug-ins and pipelines are defined as follows:

package plugin
...
// The plug-in abstracts the interface definition
type Plugin interface {}
// Input plug-in for receiving messages
type Input interface {
	Plugin
	Receive() string
}
// Filter plugin for processing messages
type Filter interface {
	Plugin
	Process(msg string) string
}
// Output plug-in for sending messages
type Output interface {
	Plugin
	Send(msg string)}Copy the code
package pipeline
...
// The definition of the message pipe
type Pipeline struct {
	input  plugin.Input
	filter plugin.Filter
	output plugin.Output
}
// Input -> filter -> output
func (p *Pipeline) Exec(a) {
	msg := p.input.Receive()
	msg = p.filter.Process(msg)
	p.output.Send(msg)
}
Copy the code

Next, we define the concrete implementation of input, filter and Output plug-in interfaces:

package plugin
...
// Input plugin name and type mapping, mainly used to create input objects through reflection
var inputNames = make(map[string]reflect.Type)
// The Hello Input plug-in receives the "Hello World" message
type HelloInput struct {}

func (h *HelloInput) Receive(a) string {
	return "Hello World"
}
// Initialize the INPUT plug-in mapping table
func init(a) {
	inputNames["hello"] = reflect.TypeOf(HelloInput{})
}
Copy the code
package plugin
...
// Filter plugin name and type mapping, mainly used to create filter objects through reflection
var filterNames = make(map[string]reflect.Type)
// The Upper Filter plugin converts all letters of a message to uppercase
type UpperFilter struct {}

func (u *UpperFilter) Process(msg string) string {
	return strings.ToUpper(msg)
}
// Initializes the filter plug-in mapping table
func init(a) {
	filterNames["upper"] = reflect.TypeOf(UpperFilter{})
}
Copy the code
package plugin
...
// Output plug-in name and type mapping, mainly used to create output objects through reflection
var outputNames = make(map[string]reflect.Type)
// The Console output plug-in outputs messages to the Console
type ConsoleOutput struct {}

func (c *ConsoleOutput) Send(msg string) {
	fmt.Println(msg)
}
// Initialize the output plug-in mapping table
func init(a) {
	outputNames["console"] = reflect.TypeOf(ConsoleOutput{})
}
Copy the code

Then, we define the plug-in abstract factory interface and the corresponding factory implementation of the plug-in:

package plugin
...
// Plugin abstract factory interface
type Factory interface {
	Create(conf Config) Plugin
}
// Input plugin Factory object that implements the Factory interface
type InputFactory struct{}
// Read the configuration and instantiate the object through reflection
func (i *InputFactory) Create(conf Config) Plugin {
	t, _ := inputNames[conf.Name]
	return reflect.New(t).Interface().(Plugin)
}
// Filter and Output plug-in factory implementations are similar
type FilterFactory struct{}
func (f *FilterFactory) Create(conf Config) Plugin {
	t, _ := filterNames[conf.Name]
	return reflect.New(t).Interface().(Plugin)
}
type OutputFactory struct{}
func (o *OutputFactory) Create(conf Config) Plugin {
	t, _ := outputNames[conf.Name]
	return reflect.New(t).Interface().(Plugin)
}
Copy the code

The pipelien object is instantiated using the plugin.factory abstract Factory.

package pipeline
...
// Save the factory instance used to create the Plugin, where map key is the plug-in type and value is the abstract factory interface
var pluginFactories = make(map[plugin.Type]plugin.Factory)
Plugin.type Returns a factory instance of the plugin Type
func factoryOf(t plugin.Type) plugin.Factory {
	factory, _ := pluginFactories[t]
	return factory
}
// The pipeline factory method creates a pipeline instance based on the configuration
func Of(conf Config) *Pipeline {
	p := &Pipeline{}
	p.input = factoryOf(plugin.InputType).Create(conf.Input).(plugin.Input)
	p.filter = factoryOf(plugin.FilterType).Create(conf.Filter).(plugin.Filter)
	p.output = factoryOf(plugin.OutputType).Create(conf.Output).(plugin.Output)
	return p
}
// Initialize the plug-in factory object
func init(a) {
	pluginFactories[plugin.InputType] = &plugin.InputFactory{}
	pluginFactories[plugin.FilterType] = &plugin.FilterFactory{}
	pluginFactories[plugin.OutputType] = &plugin.OutputFactory{}
}
Copy the code

The test code is as follows:

package test
...
func TestPipeline(t *testing.T) {
  // Pipeline.defaultConfig () is configured as follows:
  HelloInput -> UpperFilter -> ConsoleOutput
	p := pipeline.Of(pipeline.DefaultConfig())
	p.Exec()
}
// Run the result
=== RUN   TestPipeline
HELLO WORLD
--- PASS: TestPipeline (0.00s)
PASS
Copy the code

Prototype Pattern

The paper

The clone() method, at its core, returns a copy of a Prototype object. In programming, it is common to encounter scenarios that require a large number of identical objects. Instead of using the prototype pattern, we might create objects by creating a new instance of the same object, then iterating through all the member variables of the original object and copying the values of the member variables into the new object. The downside of this approach is that the consumer must know the implementation details of the object, leading to coupling between the code. In addition, it is very likely that the object has variables that are not visible other than the object itself, in which case this approach will not work.

In this case, a better approach is to use the prototype pattern and delegate the copying logic to the object itself, which solves both problems.

Go to realize

Using the Message example from the Builder pattern section, now design a Prototype abstract interface:

package prototype
...
// Prototype copy abstract interface
type Prototype interface {
	clone() Prototype
}

type Message struct {
	Header *Header
	Body   *Body
}

func (m *Message) clone(a) Prototype {
	msg := *m
	return &msg
}
Copy the code

The test code is as follows:

package test
...
func TestPrototype(t *testing.T) {
	message := msg.Builder().
		WithSrcAddr("192.168.0.1").
		WithSrcPort(1234).
		WithDestAddr("192.168.0.2").
		WithDestPort(8080).
		WithHeaderItem("contents"."application/json").
		WithBodyItem("record1").
		WithBodyItem("record2").
		Build()
  // Copy a message
	newMessage := message.Clone().(*msg.Message)
	ifnewMessage.Header.SrcAddr ! = message.Header.SrcAddr { t.Errorf("Clone Message failed.")}if newMessage.Body.Items[0] != message.Body.Items[0] {
		t.Errorf("Clone Message failed.")}}// Run the result
=== RUN   TestPrototype
--- PASS: TestPrototype (0.00s)
PASS
Copy the code

conclusion

This article focuses on five of GoF’s 23 design patterns, the creation patterns, each of which aims to provide a simple interface to decouple the object creation process from the consumer. The singleton pattern is mainly used to ensure that a class has only one instance and provide a global access point to access it. The builder mode mainly solves the scenarios that need to pass in multiple parameters when creating objects, or have requirements on the initialization sequence. The factory method pattern hides the details of object creation from the consumer by providing a factory object or method; Abstract factory pattern is an optimization of factory method pattern. By adding an abstraction layer to the factory object, it makes the factory object follow the principle of single responsibility, and avoids shotgun modification. The prototype pattern makes object copying much easier.

In the next article, we will introduce 7 Structural patterns among 23 design patterns and their implementation in Go language.