Abstract: Design Pattern is a set of repeatedly used, most people know, after classification, summary of code Design experience, the use of Design Pattern is to reuse the code, make the code easier to be understood by others and ensure the reliability of the code.

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

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!

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 to implement the 23 design patterns proposed by GoF, It is organized according to three categories: Creational Pattern, Structural Pattern and Behavioral Pattern. The text mainly introduces the Creational 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 misuse scenario for the singleton pattern. A better approach is to design two metrics as two instances of ApiMeticsuccess 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 ... MessagePool struct {pool *sync. pool} var msgPool = &messagepool {// If there is no message in the messagePool, Pool: &sync. pool {New: func() interface{} {return &Message{Count: 0}}}, Func Instance() *messagePool {return msgPool} // Add message func (m *messagePool) AddMsg(MSG) Func (m *messagePool) GetMsg() *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() if msg0.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() if msg1.Count ! = 1 {t. rrorf("expect MSG count %d, but actual %d.", 1, msg1.count)}} PASS TestMessagePool (0.00 s)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.

// Singleton mode "lazy mode" implement package msgpool... Var once = &sync.once {} // Message pool singleton, Func Instance() *messagePool {// Implement initialization logic in anonymous function, Do(func() {msgPool = &messagepool {// If there is no Message in the Message pool, create a New Message instance pool: &sync.pool {New: func() 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:

MSG := 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), }, 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 ... Struct {once * sync.once MSG *Message} struct {once * sync.once MSG *Message} return &builder{ once: &sync.Once{}, msg: &Message{Header: &Header{}, Body: &Body{}}, 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, Do(func() {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 = Func (b * Builder) Build() *Message {return b.sg}Copy the code

The test code is as follows:

package test ... Func TestMessageBuilder(t * test.t) {// Create object with message Builder := 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() if message.Header.SrcAddr ! = "192.168.0.1" {t. rrorf("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 TestMessageBuilder -- PASS: TestMessageBuilder (0.00s) PASSCopy 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 ... Uint8 const (Start type = iota End) // Type StartEvent struct{content string}... 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 ... 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) if e.EventType() ! = event.Start { t.Errorf("expect event.Start, but actual %v.", e.EventType()) } e = factory.Create(event.End) if e.EventType() ! = event.End { t.Errorf("expect event.End, but actual %v.", E.venttype ())}} // RUN TestEventFactory -- PASS: TestEventFactory (0.00s) PASSCopy 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 ... Func OfStart() Event{return &StartEvent{content: Func OfEnd() 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() if e.EventType() ! = event.Start { t.Errorf("expect event.Start, but actual %v.", e.EventType()) } e = event.OfEnd() if e.EventType() ! End {t. rorf("expect event.End, but actual %v.", e.ventType ())}} // RUN TestEvent -- PASS: PASS TestEvent (0.00 s)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 ... Type Input interface {Plugin Receive() string} Type Filter interface {Plugin Process(MSG string) string} Type Output interface {Plugin Send(MSG string)} Package pipeline... Struct {input plugin.input filter plugin.filter output plugin.output} filter -> output func (p *Pipeline) Exec() { 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 ... Var inputNames = make(map[string] reflect.type) // Hello input, Struct {} func (h *HelloInput) Receive() string {return "Hello World"} // Func init() {inputNames["hello"] = reflect.typeof (HelloInput{})} package plugin... Var filterNames = make(map[string] reflect.type) // Upper filter, Type UpperFilter struct {} func (u *UpperFilter) Process(MSG string) string {return strings.toupper (MSG)} Func init() {filterNames["upper"] = reflect.typeof (UpperFilter{})} package plugin... func init() {filterNames["upper"] = reflect.typeof (UpperFilter{})} package plugin... Var outputNames = make(map[string] reflect.type) // Console output Type ConsoleOutput struct {} func (c *ConsoleOutput) Send(MSG string) {fmt.println (MSG)} // Func init() {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 ... Type Factory interface {Create(conf Config) Plugin} // input Plugin Factory object, Type InputFactory struct{} Func (I *InputFactory) Create(conf Config) Plugin {t, _ := inputNames[conf.name] return reflect.new (t).interface ().(Plugin)} // Filter and output plug-in factories implement 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 the map key is the plug-in type, Var pluginFactories = make(map[plugin.type] plugin.factory) // Return the Factory instance func of the plugin.type FactoryOf (t plugin.type) plugin.factory {Factory, _ := pluginFactories[t] return Factory} Create a Pipeline instance 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} func init() { 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 ... TestPipeline(t * testing.t) {// HelloInput -> UpperFilter -> ConsoleOutput p := pipeline.of (pipeline.defaultConfig ()) p.exec ()} PASS TestPipeline (0.00 s)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 the same objects. If we do not use the prototype pattern, we might create objects like this:

Create a new instance of the same object, then iterate over all the member variables of the original object and copy the member variable values 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 interface {clone() Prototype} type Message struct {Header *Header Body *Body} func (m) *Message) clone() Prototype { msg := *m return &msg }Copy the code

The test code is as follows:

package test ... Func TestPrototype(t * test.t) {message := msg.builder ().withsrcaddr ("192.168.0.1").withsrcPort (1234). WithDestAddr (" 192.168.0.2 "). WithDestPort (8080). WithHeaderItem (" contents ", WithBodyItem("record1").withBodyItem ("record2").build () // Copy a message newMessage := message.Clone().(*msg.Message) if newMessage.Header.SrcAddr ! = message.Header.SrcAddr { t.Errorf("Clone Message failed.") } if newMessage.Body.Items[0] ! Items[0] {t. rrorf("Clone message failed.")}} // RUN TestPrototype === RUN prototype -- PASS: PASS TestPrototype (0.00 s)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.

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