Come on, here are the Go language implementations of 23 design patterns

华为云开发者社区
中文
Abstract: design pattern (Design Pattern) is a set of repeated use, most people know, after classification, a summary of code design experience, the use of design patterns is to reusable code, make the code easier to understand by others and Ensure code reliability.

This article is shared from the HUAWEI cloud community " coming soon, here are the Go language implementations of 23 design patterns ", the original author: Yuan Runzi.

Preface

25 years have passed since GoF proposed 23 design patterns in 1995, and design patterns are still a hot topic in the software field. At the moment, if you don't know a little design pattern, you are embarrassed to say that you are a qualified programmer. Design patterns are usually defined as:

Design Pattern (Design Pattern) is a set of code design experience that is used repeatedly, known to most people, cataloged, and summarized. The use of design patterns is to reusable code, make the code easier to understand by others, and ensure code reliability. .

From a definition point of view, the design pattern is actually a summary of experience, a simple and elegant solution for specific problems . Since it is a summary of experience, the most direct benefit of learning design patterns is that you can stand on the shoulders of giants to solve some specific problems in the software development process. However, the highest level of learning design patterns is to learn the ideas used to solve the problems. When you understand their essential ideas, you can achieve even if you have forgotten the name and structure of a design pattern, you can still It is easy to solve a specific problem.

Good things are touted, and of course they will be black. Design patterns are criticized mainly because of the following two points:

1. Design patterns will increase the amount of code and complicate program logic. This is inevitable, but we can't just consider the cost of the development phase. The simplest program is of course a function written from the beginning to the end, but the maintenance cost in the later period will become very large; and although the design pattern increases a little development cost, it allows people to write reusable and maintainable program. To quote the concepts in "The Philosophy of Software Design", the former is tactical programming, and the latter is strategic programming. We should say No to tactical programming!

2. Abuse of design patterns. This is the easiest mistake for beginners. When you learn a pattern, you can't wait to use it in all the code, so you deliberately use the pattern where you shouldn't use the pattern, causing the program to become extremely complicated. In fact, every design pattern has several key elements: applicable scenarios, solutions, advantages and disadvantages. Model is not a panacea, it can only show effect on specific problems. So, before using a mode, ask yourself, is this mode suitable for the current scenario?

The subtitle of the book "Design Patterns" is "The Foundation of Reusable Object-Oriented Software", but it does not mean that only object-oriented languages can use design patterns. A model is just a kind of thinking to solve a specific problem, and has nothing to do with language. Just like the Go language, it is not an object-oriented language like C++ and Java, but the design patterns are also applicable. This series of articles will use Go language to implement the 23 design patterns proposed by GoF, organized according to three categories: Creational Pattern, Structural Pattern and Behavioral Pattern. The text mainly introduces One of the creational mode.

Singleton Pattern

Brief introduction

The singleton mode is the simplest of the 23 design modes. It is mainly used for ensure that there is only one instance of a class and provide a global access point to access it.

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

However, not all globally unique scenarios are suitable for singleton mode. For example, consider the situation where an API call needs to be counted. There are two indicators, the number of successful calls and the number of failed calls. These two metrics are globally unique, so someone might model them as two singletons, SuccessApiMetric and FailApiMetric. According to this idea, as the number of indicators increases, you will find more and more class definitions in the code, and more and more bloated. This is also the most common misuse scenario of the singleton mode. A better way is to design the two indicators into two instances ApiMetic success and ApiMetic fail under one object ApiMetric.

How to judge whether an object should be modeled as a singleton?

Generally, objects modeled as singletons have the meaning of "central point". For example, the thread pool is the center for managing all threads. Therefore, when judging whether an object is suitable for singleton mode, think first, is this object a central point?

Go implementation

When implementing the singleton mode for an object, two points must be noted: (1) Restrict the caller to instantiate the object directly; (2) Provide a globally unique access method for the singleton of the object.

For C++/Java, you only need to design the constructor of the class to be private and provide a static method to access the unique instance of the class. But for the Go language, there is no concept of a constructor, and no static method, so you need to find another way out.

We can use the access rules of the Go language package to achieve this. By designing the singleton structure to lowercase the first letter, we can limit its access range to only under the current package, simulating the private constructor in C++/Java; and then in the current package The next implementation of an access function with the first letter capitalized is equivalent to the function of the static method.

In actual development, we often encounter objects that need to be created and destroyed frequently. Frequent creation and destruction consumes CPU, and memory utilization is not high. We usually use object pool technology to optimize. Consider that we need to implement a message object pool, because it is the global central point that manages all Message instances, so it is implemented as a singleton. The implementation code is as follows:

 package msgpool
 ...
 // 消息池
 type messagePool struct {
 pool *sync.Pool
 }
 // 消息池单例
 var msgPool = &messagePool{
 // 如果消息池里没有消息,则新建一个Count值为0的Message实例
 pool: &sync.Pool{New: func() interface{} { return &Message{Count: 0} }},
 }
 // 访问消息池单例的唯一方法
 func Instance() *messagePool {
 return msgPool
 }
 // 往消息池里添加消息
 func (m *messagePool) AddMsg(msg *Message) {
 m.pool.Put(msg)
 }
 // 从消息池里获取消息
 func (m *messagePool) GetMsg() *Message {
 return m.pool.Get().(*Message)
 }
 ...

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.Errorf("expect msg count %d, but actual %d.", 1, msg1.Count)
 }
 }
 // 运行结果
 === RUN   TestMessagePool
 --- PASS: TestMessagePool (0.00s)
 PASS

The above singleton mode is a typical "hungry man mode", the instance has been initialized when the system is loaded. Correspondingly, there is also a "lazy man mode". Only when the object is used will it be initialized, which saves memory to a certain extent. As we all know, the "lazy man mode" will bring thread safety issues, which can be optimized by ordinary locking or a more efficient double check lock. For the "lazy man mode", Go language has a more elegant way to implement it, that is, using sync.Once, it has a Do method whose input parameter is a method, and Go language guarantees that this method is only called once.

 // 单例模式的“懒汉模式”实现
 package msgpool
 ...
 var once = &sync.Once{}
 // 消息池单例,在首次调用时初始化
 var msgPool *messagePool
 // 全局唯一获取消息池pool到方法
 func Instance() *messagePool {
 // 在匿名函数中实现初始化逻辑,Go语言保证只会调用一次
 once.Do(func() {
 msgPool = &messagePool{
 // 如果消息池里没有消息,则新建一个Count值为0的Message实例
 pool: &sync.Pool{New: func() interface{} { return &Message{Count: 0} }},
 }
 })
 return msgPool
 }
 ...

Builder Pattern

Brief introduction

In programming, we will often encounter some complex objects, which have many member properties, and even nested multiple complex objects. In this case, creating this complex object will become very cumbersome. For C++/Java, the most common manifestation is that the constructor has a long parameter list:

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

For the Go language, the most common manifestation is multi-level nested instantiation:

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

The above object creation method has two obvious shortcomings: (1) It is not friendly to the object user, and the user needs to know too many details when creating the object; (2) The code is very readable.

For this kind of scenes with many object members and cumbersome object creation logic, it is suitable to use the builder mode for optimization.

The functions of the builder mode are as follows:

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

2. You can assign values to members step by step in order, 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 mode below is mainly for examples of these two points.

Go implementation

Consider the following Message structure, which is mainly composed of Header and 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
 }
 ...

If you follow the direct object creation method, the creation logic should be like this:

 // 多层的嵌套实例化
 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),
 },
 }
 // 需要知道对象的实现细节
 message.Header.Items["contents"] = "application/json"
 message.Body.Items = append(message.Body.Items, "record1")
 message.Body.Items = append(message.Body.Items, "record2")

Although the nesting level of the Message structure is not much, judging from the code it creates, it does have the disadvantages of being unfriendly to object users and poor code readability. Below we introduce the builder mode to refactor the code:

package msg
 ...
 // Message对象的Builder对象
 type builder struct {
 once *sync.Once
 msg *Message
 }
 // 返回Builder对象
 func Builder() *builder {
 return &builder{
 once: &sync.Once{},
 msg: &Message{Header: &Header{}, Body: &Body{}},
 }
 }
 // 以下是对Message成员对构建方法
 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 {
   // 保证map只初始化一次
 b.once.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 = append(b.msg.Body.Items, record)
 return b
 }
 // 创建Message对象,在最后一步调用
 func (b *builder) Build() *Message {
 return b.msg
 }

The test code is as follows:

package test
 ...
 func TestMessageBuilder(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()
 if message.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   TestMessageBuilder
 --- PASS: TestMessageBuilder (0.00s)
 PASS

From the test code, we can see that using the builder mode to create objects, users no longer need to know the specific implementation details of the object, and the code is more readable.

Factory Method Pattern

Brief introduction

The factory method pattern is similar to the builder pattern discussed in the previous section. Both encapsulate the logic of object creation and provide users with a simple and easy-to-use object creation interface. The two are slightly different in application scenarios. The builder mode is more commonly used in scenarios where multiple parameters need to be passed for instantiation.

There are two main benefits of using factory methods to create objects:

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

2. Decouple from user code. many cases, the creation of an object is often a point that is easy to change. The factory method is used to encapsulate the creation process of the object, which can avoid shotgun modification when the creation logic changes.

There are also two ways to implement the factory method pattern: (1) Provide a factory object to create a product object by calling the factory method of the factory object; (2) Integrate the factory method into the product object (static method of the object in C++/Java, Functions under the same package in Go)

Go implementation

Consider an event object Event, which has two valid time types Start and End:

package event
 ...
 type Type uint8
 // 事件类型定义
 const (
 Start Type = iota
 End
 )
 // 事件抽象接口
 type Event interface {
 EventType() Type
 Content() string
 }
 // 开始事件,实现了Event接口
 type StartEvent struct{
 content string
 }
 ...
 // 结束事件,实现了Event接口
 type EndEvent struct{
 content string
 }
 ...

1. According to the first implementation method, a factory object is provided for Event. The specific code is as follows:

 package event
 ...
 // 事件工厂对象
 type Factory struct{}
 // 更具事件类型创建具体事件
 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
 }
 }

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.EventType())
 }
 }
 // 运行结果
 === RUN   TestEventFactory
 --- PASS: TestEventFactory (0.00s)
 PASS

2. According to the second implementation method, a factory method is provided separately for the Start and End types of Event. The code is as follows:

package event
 ...
 // Start类型Event的工厂方法
 func OfStart() Event {
 return &StartEvent{
 content: "this is start event",
 }
 }
 // End类型Event的工厂方法
 func OfEnd() Event {
 return &EndEvent{
 content: "this is end event",
 }
 }

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() != event.End {
 t.Errorf("expect event.End, but actual %v.", e.EventType())
 }
 }
 // 运行结果
 === RUN   TestEvent
 --- PASS: TestEvent (0.00s)
 PASS

Abstract Factory Pattern

Brief introduction

In the factory method pattern, we create a product family through a factory object, and the specific product to create is judged by the swtich-case method. This also means that for each new type of product object in the product group, the code of the original factory object must be modified; and with the continuous increase of products, the responsibilities of the factory object become heavier and heavier, which violates the single responsibility principle.

The abstract factory pattern solves this problem by adding an abstract layer to the factory class. As shown in the figure above, both FactoryA and FactoryB implement the abstract factory interface, which is used to create ProductA and ProductB, respectively. If ProductC is added in the future, just add a FactoryC without modifying the original code; because each factory is only responsible for creating one product, it also follows the single responsibility principle.

Go implementation

Consider the need for a message processing system with the following plug-in architecture style. Pipeline is a message processing pipeline, which contains three plug-ins: input, filter, and output. We need to create a pipeline based on the configuration. The implementation of the process of loading plug-ins is very suitable for using the factory pattern. The creation of input, filter and output plug-ins uses the abstract factory pattern, and the creation of the pipeline uses the factory method pattern.

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

package plugin
 ...
 // 插件抽象接口定义
 type Plugin interface {}
 // 输入插件,用于接收消息
 type Input interface {
 Plugin
 Receive() string
 }
 // 过滤插件,用于处理消息
 type Filter interface {
 Plugin
 Process(msg string) string
 }
 // 输出插件,用于发送消息
 type Output interface {
 Plugin
 Send(msg string)
 }
package pipeline
 ...
 // 消息管道的定义
 type Pipeline struct {
 input  plugin.Input
 filter plugin.Filter
 output plugin.Output
 }
 // 一个消息的处理流程为 input -> filter -> output
 func (p *Pipeline) Exec() {
 msg := p.input.Receive()
 msg = p.filter.Process(msg)
 p.output.Send(msg)
 }

Next, we define the specific implementation of the input, filter, and output plug-in interfaces:

package plugin
 ...
 // input插件名称与类型的映射关系,主要用于通过反射创建input对象
 var inputNames = make(map[string]reflect.Type)
 // Hello input插件,接收“Hello World”消息
 type HelloInput struct {}
 ​
 func (h *HelloInput) Receive() string {
 return "Hello World"
 }
 // 初始化input插件映射关系表
 func init() {
 inputNames["hello"] = reflect.TypeOf(HelloInput{})
 }
 package plugin
 ...
 // filter插件名称与类型的映射关系,主要用于通过反射创建filter对象
 var filterNames = make(map[string]reflect.Type)
 // Upper filter插件,将消息全部字母转成大写
 type UpperFilter struct {}
 ​
 func (u *UpperFilter) Process(msg string) string {
 return strings.ToUpper(msg)
 }
 // 初始化filter插件映射关系表
 func init() {
 filterNames["upper"] = reflect.TypeOf(UpperFilter{})
 }
 package plugin
 ...
 // output插件名称与类型的映射关系,主要用于通过反射创建output对象
 var outputNames = make(map[string]reflect.Type)
 // Console output插件,将消息输出到控制台上
 type ConsoleOutput struct {}
 ​
 func (c *ConsoleOutput) Send(msg string) {
 fmt.Println(msg)
 }
 // 初始化output插件映射关系表
 func init() {
 outputNames["console"] = reflect.TypeOf(ConsoleOutput{})
 }

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

package plugin
 ...
 // 插件抽象工厂接口
 type Factory interface {
 Create(conf Config) Plugin
 }
 // input插件工厂对象,实现Factory接口
 type InputFactory struct{}
 // 读取配置,通过反射机制进行对象实例化
 func (i *InputFactory) Create(conf Config) Plugin {
 t, _ := inputNames[conf.Name]
 return reflect.New(t).Interface().(Plugin)
 }
 // filter和output插件工厂实现类似
 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)
 }

Finally, define the factory method of the pipeline and call the plugin.Factory abstract factory to complete the instantiation of the pipeline object:

package pipeline
 ...
 // 保存用于创建Plugin的工厂实例,其中map的key为插件类型,value为抽象工厂接口
 var pluginFactories = make(map[plugin.Type]plugin.Factory)
 // 根据plugin.Type返回对应Plugin类型的工厂实例
 func factoryOf(t plugin.Type) plugin.Factory {
 factory, _ := pluginFactories[t]
 return factory
 }
 // pipeline工厂方法,根据配置创建一个Pipeline实例
 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{}
 }

The test code is as follows:

package test
 ...
 func TestPipeline(t *testing.T) {
   // 其中pipeline.DefaultConfig()的配置内容见【抽象工厂模式示例图】
   // 消息处理流程为 HelloInput -> UpperFilter -> ConsoleOutput
 p := pipeline.Of(pipeline.DefaultConfig())
 p.Exec()
 }
 // 运行结果
 === RUN   TestPipeline
 HELLO WORLD
 --- PASS: TestPipeline (0.00s)
 PASS

Prototype Pattern

Brief introduction

The prototype mode mainly solves the problem of object replication. Its core is the clone() method, which returns a copy of the Prototype object. In the process of programming, there are often scenes that require a large number of the same objects. If the prototype mode is not used, then we may create objects like this: create a new instance of the same object, and then traverse all the members of the original object Variable, and copy the member variable value to the new object. The shortcoming of this method is obvious, that is, the user must know the implementation details of the object, which leads to the coupling between the codes. In addition, the object is likely to have invisible variables other than the object itself, in which case this method will not work.

In this case, a better way is to use the prototype model to delegate the replication logic to the object itself. In this way, the above two problems are also solved.

Go implementation

Still take the Message in the builder pattern section as an example, and now design a prototype abstract interface:

package prototype
 ...
 // 原型复制抽象接口
 type Prototype interface {
 clone() Prototype
 }
 ​
 type Message struct {
 Header *Header
 Body   *Body
 }
 ​
 func (m *Message) clone() Prototype {
 msg := *m
 return &msg
 }

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()
   // 复制一份消息
 newMessage := message.Clone().(*msg.Message)
 if newMessage.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   TestPrototype
 --- PASS: TestPrototype (0.00s)
 PASS

to sum up

This article mainly introduces 5 creational modes of GoF's 23 design modes. The purpose of the creational mode is to provide a simple interface to decouple the object creation process from the user. Among them, the singleton mode is mainly used to ensure that there is only one instance of a class, and to provide a global access point to access it; the builder mode mainly solves scenarios where multiple parameters need to be passed in when an object is created, or the initialization sequence is required. ; The factory method pattern hides the details of object creation for users by providing a factory object or factory method; the abstract factory pattern is an optimization of the factory method pattern, by adding an abstract layer to the factory object, let the factory object follow a single responsibility The principle also avoids shotgun modification; the prototype mode makes object copying easier.

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

阅读 1.5k

开发者之家
华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态...

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态...

1.3k 声望
1.7k 粉丝
0 条评论

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态...

1.3k 声望
1.7k 粉丝
文章目录
宣传栏