preface

In the last article, 23 Design Patterns for GoF with Go (Part ii), we introduced composition, adapter, and bridge patterns in Structural patterns. This article covers the remaining structural patterns, proxy, decorator, facade, and share. This paper will continue to use message processing system as an example. If you are not clear about this example, please refer to “23 Design Patterns for GoF With Go (I)” and “23 Design Patterns for GoF with Go (II)” to learn about their related design and implementation.

Proxy Pattern

Introduction to the

The proxy pattern, which provides a proxy for an object to control access to that object, is a highly used design pattern that is common even in real life, such as concert ticket scalpers. Let’s say you need to see a concert, but tickets are sold out on the website, so you go to the concert that day and buy one at a high price from a scalper. In this example, a scalper acts as an agent for concert tickets, and you accomplish this goal through an agent when tickets cannot be purchased through official channels.

As we can see from the concert ticket example, the key to using proxy mode is to provide a proxy object to control access to an object when it is not convenient for clients to directly access that object. The Client actually accesses the proxy object, which forwards the Client’s request to the ontology object for processing.

In programming, there are several proxy modes:

1. Remote proxy. Remote proxy applies to the object that provides services on a remote machine, and the service cannot be used through ordinary function calls. Because ontology objects cannot be accessed directly, all remote proxy objects usually do not directly hold a reference to the ontology object, but hold the address of the remote machine to access the ontology object through the network protocol.

2. Virtual proxy: There are often some heavyweight service objects in program design. If the object instance is held all the time, it will consume system resources.

3. Protection Proxy. It is used to control access to ontology objects and is often used in scenarios where permission verification is required for Client access.

4. Cache proxy. Cache proxy mainly adds a layer of cache between Client and ontology object to accelerate the access of ontology object, which is commonly seen in the scenario of connecting database.

Smart reference provides additional actions for the access of ontology objects. Its common implementation is the smart pointer in C++, which provides counting function for the access of objects. When the count of the access object is 0, the object is destroyed.

All of these agents have the same implementation principle, and we will introduce the Go language implementation of remote agents.

Go to realize

Consider storing the message processing system output to a database with the following interface:

package db
...
// key-value Indicates the database interface
type KvDb interface {
	// Store data
	// Reply is the result of the operation. If the operation succeeds, it is true; otherwise, it is false
	// Return error on failure to connect to database, nil on success
	Save(record Record, reply *bool) error
	// Get value by key, where value is returned by the pointer type in the function argument
	// Return error on failure to connect to database, nil on success
	Get(key string, value *string) error
}

type Record struct {
	Key   string
	Value string
}
Copy the code

Database is a key-value database, which uses map to store data. Db. Server implements db.KvDb interface.

package db
...
// Database server implementation
type Server struct {
	// Use map to store key-value data
	data map[string]string
}

func (s *Server) Save(record Record, reply *bool) error {
	if s.data == nil{
		s.data = make(map[string]string)
	}
	s.data[record.Key] = record.Value
	*reply = true
	return nil
}

func (s *Server) Get(key string, reply *string) error {
	val, ok := s.data[key]
	if! ok { *reply =""
		return errors.New("Db has no key " + key)
	}
	*reply = val
	return nil
}
Copy the code

The message processing system and the database are not on the same machine, so the message processing system cannot directly call the db.server method for data storage, such as the service provider and service consumer are not on the same machine, the use of remote proxy is best suited.

One of the most common implementations of Remote proxies is Remote Procedure Call (RPC), which allows a client application to directly Call the methods of a server application on a different machine as if it were calling a local object. In the Go language field, in addition to the famous gRPC, Go standard library NET/RPC package also provides the implementation of RPC. Below, we provide database server capabilities externally through NET/RPC:

package db
...
// Start the database and provide an RPC interface to access the database
func Start(a) {
	rpcServer := rpc.NewServer()
	server := &Server{data: make(map[string]string)}
  // Register the database interface with the RPC server
	iferr := rpcServer.Register(server); err ! =nil {
		fmt.Printf("Register Server to rpc failed, error: %v", err)
		return
	}
	l, err := net.Listen("tcp"."127.0.0.1:1234")
	iferr ! =nil {
		fmt.Printf("Listen tcp failed, error: %v", err)
		return
	}
	go rpcServer.Accept(l)
	time.Sleep(1 * time.Second)
	fmt.Println("Rpc server start success.")}Copy the code

So far, we have provided external access to the database. Now we need a remote agent to connect to the database server and perform related database operations. For a message-processing system, it does not need, nor should it know, the low-level details of the interaction between the remote agent and the database server, thus reducing the coupling between the systems. Therefore, the remote agent needs to implement db.kvdb:

package db
...
// Database server remote proxy, implement db.kvDB interface
type Client struct {
	// RPC client
	cli *rpc.Client
}

func (c *Client) Save(record Record, reply *bool) error {
	var ret bool
	// Call the server interface through RPC
	err := c.cli.Call("Server.Save", record, &ret)
	iferr ! =nil {
		fmt.Printf("Call db Server.Save rpc failed, error: %v", err)
		*reply = false
		return err
	}
	*reply = ret
	return nil
}

func (c *Client) Get(key string, reply *string) error {
	var ret string
	// Call the server interface through RPC
	err := c.cli.Call("Server.Get", key, &ret)
	iferr ! =nil {
		fmt.Printf("Call db Server.Get rpc failed, error: %v", err)
		*reply = ""
		return err
	}
	*reply = ret
	return nil
}

// Factory method that returns the remote proxy instance
func CreateClient(a) *Client {
	rpcCli, err := rpc.Dial("tcp"."127.0.0.1:1234")
	iferr ! =nil {
		fmt.Printf("Create rpc client failed, error: %v.", err)
		return nil
	}
	return &Client{cli: rpcCli}
}
Copy the code

The remote proxy db.client does not hold a reference to db.Server directly, but its IP :port and calls its methods through the RPC Client.

Next, we need to implement a new Output plug-in, DbOutput, for the message processing system, calling the db.client remote proxy to store messages to the database.

In “23 Design Patterns for GoF with Go (ii)”, we introduced three life-cycle methods Start, Stop and Status for the Plugin. Each new plug-in needs to implement these three methods. But the logic of the three methods in most plug-ins is basically the same, leading to a degree of code redundancy. What’s a good solution to the repetitive code problem? Combination mode!

Next, we’ll use the composite pattern to extract this method into a new object LifeCycle so that when a plug-in is added LifeCycle can be added as an anonymous member (embedded composite) and the redundant code can be resolved.

package plugin
...
type LifeCycle struct {
	name   string
	status Status
}

func (l *LifeCycle) Start(a) {
	l.status = Started
	fmt.Printf("%s plugin started.\n", l.name)
}

func (l *LifeCycle) Stop(a) {
	l.status = Stopped
	fmt.Printf("%s plugin stopped.\n", l.name)
}

func (l *LifeCycle) Status(a) Status {
	return l.status
}
Copy the code

The implementation of DbOutput is as follows, which holds a remote proxy that stores messages to a remote database.

package plugin
...
type DbOutput struct {
	LifeCycle
	// Operate the remote agent for the database
	proxy db.KvDb
}

func (d *DbOutput) Send(msg *msg.Message) {
	ifd.status ! = Started { fmt.Printf("%s is not running, output nothing.\n", d.name)
		return
	}
	record := db.Record{
		Key:   "db",
		Value: msg.Body.Items[0],
	}
	reply := false
	err := d.proxy.Save(record, &reply)
	iferr ! =nil| |! reply { fmt.Println("Save msg to db server failed.")}}func (d *DbOutput) Init(a) {
	d.proxy = db.CreateClient()
	d.name = "db output"
}
Copy the code

The test code is as follows:

package test
...
func TestDbOutput(t *testing.T) {
	db.Start()
	config := pipeline.Config{
		Name: "pipeline3",
		Input: plugin.Config{
			PluginType: plugin.InputType,
			Name:       "hello",
		},
		Filter: plugin.Config{
			PluginType: plugin.FilterType,
			Name:       "upper",
		},
		Output: plugin.Config{
			PluginType: plugin.OutputType,
			Name:       "db",
		},
	}
	p := pipeline.Of(config)
	p.Start()
	p.Exec()

	// Verify that DbOutput is stored correctly
	cli := db.CreateClient()
	var val string
	err := cli.Get("db", &val)
	iferr ! =nil {
		t.Errorf("Get db failed, error: %v\n.", err)
	}
	ifval ! ="HELLO WORLD" {
		t.Errorf("expect HELLO WORLD, but actual %s.", val)
	}
}
// Run the result=== RUN TestDbOutput Rpc server start success. db output plugin started. upper filter plugin started. hello input plugin  started. Pipeline started. --- PASS: TestDbOutput (1.01s)
PASS
Copy the code

Decorator Pattern

Introduction to the

In the program design, we often need to add new behavior for the object, many students’ first idea is to extend the ontology object, through inheritance to achieve the purpose. But using inheritance inevitably has two drawbacks :(1) inheritance is static, determined at compile time, and cannot change the behavior of an object at run time. (2) A subclass can only have one parent class. When too many new functions need to be added, the number of classes will increase dramatically.

For this scenario, we usually use the Decorator Pattern, which uses composition rather than inheritance to dynamically overlay new behavior on ontology objects. In theory, as long as there are no limits, it can keep adding functionality. The most classic application of decoration mode is Java I/O flow system. Through decoration mode, users can dynamically add functions for the original INPUT and output streams, such as input and output according to strings, adding caching, etc., making the whole I/O flow system has high scalability and flexibility.

The decorative pattern and the proxy pattern are structurally very similar, but they emphasize different points. The former emphasizes adding new functions to ontology objects, while the latter emphasizes access control to ontology objects. Of course, intelligent references in the proxy pattern look exactly the same to me as the decorator pattern.

Go to realize

Consider adding a feature to a Message processing system that counts how many messages are produced by each Input source, i.e. the number of messages produced by each Input. The simplest approach would be to do a dot count in the Receive method for each Input, but this leads to coupling between the statistical code and the business code. If the statistical logic changes, shotgun changes will occur, and the code becomes harder to maintain as the Input type increases.

A better approach is to put the statistical logic in one place and do the statistics every time the Receive method for Input is called. This fits nicely with decorator mode, which provides dotting statistics (new behavior) for the Input (ontology object). We can design an InputMetricDecorator as an Input decorator to do the logic of dot-count in the decorator.

First, we need to design an object that counts the number of messages generated by each Input. This object should be globally unique, so the singleton pattern is implemented:

package metric
...
// Input source statistics, designed as a singleton
type input struct {
	// Stores statistical results. The key is Input type, such as Hello and kafka
	// value indicates the statistics of the corresponding Input messages
	metrics map[string]uint64
	// Add a lock for statistics
	mu      *sync.Mutex
}

// Increment the count of the Input message named inputName by one
func (i *input) Inc(inputName string) {
	i.mu.Lock()
	defer i.mu.Unlock()
	if_, ok := i.metrics[inputName]; ! ok { i.metrics[inputName] =0
	}
	i.metrics[inputName] = i.metrics[inputName] + 1
}

// Output the current status of all points
func (i *input) Show(a) {
	fmt.Printf("Input metric: %v\n", i.metrics)
}

/ / the singleton
var inputInstance = &input{
	metrics: make(map[string]uint64),
	mu:      &sync.Mutex{},
}

func Input(a) *input {
	return inputInstance
}
Copy the code

Next we start implementing the InputMetricDecorator, which implements the Input interface and holds an ontology object Input. The InputMetricDecorator calls the Receive method of the ontology Input in the Receive method and completes the statistical action.

package plugin
...
type InputMetricDecorator struct {
	input Input
}

func (i *InputMetricDecorator) Receive(a) *msg.Message {
	// Call the Receive method of the ontology object
	record := i.input.Receive()
	// Complete the statistical logic
	if inputName, ok := record.Header.Items["input"]; ok {
		metric.Input().Inc(inputName)
	}
	return record
}

func (i *InputMetricDecorator) Start(a) {
	i.input.Start()
}

func (i *InputMetricDecorator) Stop(a) {
	i.input.Stop()
}

func (i *InputMetricDecorator) Status(a) Status {
	return i.input.Status()
}

func (i *InputMetricDecorator) Init(a) {
	i.input.Init()
}

// The factory method completes the decorator creation
func CreateInputMetricDecorator(input Input) *InputMetricDecorator {
	return &InputMetricDecorator{input: input}
}
Copy the code

Finally, we add the InputMetricDecorator agent to the Pipeline factory method for the ontology Input:

package pipeline
...
// Create 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)
	// Add an InputMetricDecorator to the ontology Input
	p.input = plugin.CreateInputMetricDecorator(p.input)
	return p
}
Copy the code

The test code is as follows:

package test
...
func TestInputMetricDecorator(t *testing.T) {
	p1 := pipeline.Of(pipeline.HelloConfig())
	p2 := pipeline.Of(pipeline.KafkaInputConfig())
	p1.Start()
	p2.Start()
	p1.Exec()
	p2.Exec()
	p1.Exec()

	metric.Input().Show()
}
// Run the result=== RUN TestInputMetricDecorator Console output plugin started. Upper filter plugin started. Hello input plugin started.  Pipeline started. Console output plugin started. Upper filter plugin started. Kafka input plugin started. Pipeline started. Output: Header:map[content:text input:hello], Body:[HELLO WORLD]
Output:
	Header:map[content:text input:kafka], Body:[I AM MOCK CONSUMER.]
Output:
	Header:map[content:text input:hello], Body:[HELLO WORLD]
Input metric: map[hello:2 kafka:1]
--- PASS: TestInputMetricProxy (0.00s)
PASS
Copy the code

Facade Pattern

Introduction to the

From the perspective of structure, the appearance mode is very simple, it mainly provides a higher level of external unified interface for the subsystem, so that clients can use the functions of the subsystem more friendly. In the figure, Subsystem Class is an abbreviation for an object in a Subsystem. It may be a single object or a collection of dozens of objects. The Facade pattern reduces the coupling between Client and Subsystem. As long as the Facade remains the same, no matter how the Subsystem changes, it is insensitive to the Client.

Appearance model in program design with very much, for example, we click the buy button in the mall, for buyers, only to see the buy the uniform interface, but for mall system, its internal conducted a series of business processes, such as inventory check, order processing, payment, logistics and so on. The facade pattern greatly improves the user experience and frees users from complex business processes.

The appearance pattern is often used in hierarchical architecture. Usually, we provide one or more unified external access interfaces for each level in the hierarchical architecture, so as to lower the coupling between different levels and make the system architecture more reasonable.

Go to realize

The facade pattern is also simple to implement, but consider the previous message processing system. In Pipeline, each message is processed by Input->Filter->Output. The code is implemented like this:

p := pipeline.Of(config)
message := p.input.Receive()
message = p.filter.Process(message)
p.output.Send(message)
Copy the code

However, the Pipeline consumer may not care how the message is processed, just that the message has been processed by the Pipeline. Therefore, we need to design a simple external interface:

package pipeline
...
func (p *Pipeline) Exec(a) {
	msg := p.input.Receive()
	msg = p.filter.Process(msg)
	p.output.Send(msg)
}
Copy the code

In this way, the user simply calls the Exec method to complete a processing of the message. The test code is as follows:

package test
...
func TestPipeline(t *testing.T) {
	p := pipeline.Of(pipeline.HelloConfig())
	p.Start()
  // Call the Exec method to process a message
	p.Exec()
}
// Run the result
=== RUN   TestPipeline
console output plugin started.
upper filter plugin started.
hello input plugin started.
Pipeline started.
Output:
	Header:map[content:text input:hello], Body:[HELLO WORLD]
--- PASS: TestPipeline (0.00s)
PASS
Copy the code

Flyweight Pattern

Introduction to the

In programming, we often encounter some very heavy objects, they usually have many member attributes, when the system is flooded with a large number of these objects, the system memory will be under great pressure. In addition, the frequent creation of these objects is a significant drain on the CPU of the system. In many cases, most of the member attributes of these heavy objects are fixed. In this case, you can use the share mode to optimize and design the fixed parts as shared objects (share elements, flyweight), which can save a lot of system memory and CPU.

Instead of storing all data in each object, the meta-mode lets you load more objects into your limited memory by sharing the same state shared by multiple objects.

When we decide to optimize a heavy object using the share mode, we first need to divide the properties of the heavy object into two categories, those that can be shared and those that cannot be shared. The intrinsic state is stored in the intrinsic element and does not change with the context in which the intrinsic element resides. The latter is called extrinsic state and its value depends on the context of the player and therefore cannot be shared. For example, both article A and article B quote picture A. Since the text contents of article A and article B are different, the text is an external state and cannot be shared. However, the picture A referenced by them is the same and belongs to the internal state, so picture A can be designed as A share element

The factory mode is usually paired with the share mode. The share factory provides a unique interface to obtain the share object, so that the Client cannot perceive how the share is shared, reducing the coupling of modules. The share pattern is somewhat similar to the singleton pattern in that objects are shared across the system, but the singleton pattern is more concerned with creating objects only once in the system, whereas the share pattern is more concerned with sharing the same state across multiple objects.

Go to realize

Suppose you want to design a system for recording player information, team information, and game results in the NBA.

The Team data structure is defined as follows:

package nba
...
type TeamId uint8

const (
	Warrior TeamId = iota
	Laker
)

type Team struct {
	Id      TeamId    / / ID team
	Name    string    // Team name
	Players []*Player // Players on the team
}
Copy the code

The Player data structure is defined as follows:

package nba
...
type Player struct {
	Name string // Player name
	Team TeamId // The ID of the player's team
}
Copy the code

The data structure of Match result is defined as follows:

package nba
...
type Match struct {
	Date         time.Time // Match time
	LocalTeam    *Team     // Home team
	VisitorTeam  *Team     // Away team
	LocalScore   uint8     // The home team scores
	VisitorScore uint8     // The away team scores
}

func (m *Match) ShowResult(a) {
	fmt.Printf("%s VS %s - %d:%d\n", m.LocalTeam.Name, m.VisitorTeam.Name,
		m.LocalScore, m.VisitorScore)
}
Copy the code

A Match in the NBA is played by two teams, the home Team and the away Team, and the corresponding code is that a Match instance holds two Team instances. Currently, the NBA consists of 30 teams, each playing 82 regular season games, a total of 2,460 games in a season, corresponding to 4,920 Team instances. However, the 30 teams in NBA are fixed. In fact, only 30 Team instances can record all the game information of a season completely. The remaining 4,890 Team instances are redundant data.

In this scenario, it is appropriate to use the share mode for optimization, and we designed the Team as a share between multiple Match instances. TeamFactory is defined as follows. The Client uses teamFactory.TeamOf to obtain the Team instance. Each Team instance is created only once and then added to the Team pool, and subsequent fetches are taken directly from the pool, thus achieving the purpose of sharing.

package nba
...
type teamFactory struct {
	// Team pool, cache team instances
	teams map[TeamId]*Team
}

// Get the Team instance from the pool according to TeamId, or create it if there is none in the pool
func (t *teamFactory) TeamOf(id TeamId) *Team {
	team, ok := t.teams[id]
	if! ok { team = createTeam(id) t.teams[id] = team }return team
}

// A single example of a factory
var factory = &teamFactory{
	teams: make(map[TeamId]*Team),
}

func Factory(a) *teamFactory {
	return factory
}

// Create a Team instance according to TeamId, only called in the TeamOf method, not visible externally
func createTeam(id TeamId) *Team {
	switch id {
	case Warrior:
		w := &Team{
			Id:      Warrior,
			Name:    "Golden State Warriors",
		}
		curry := &Player{
			Name: "Stephen Curry",
			Team: Warrior,
		}
		thompson := &Player{
			Name: "Klay Thompson",
			Team: Warrior,
		}
		w.Players = append(w.Players, curry, thompson)
		return w
	case Laker:
		l := &Team{
			Id:      Laker,
			Name:    "Los Angeles Lakers",
		}
		james := &Player{
			Name: "LeBron James",
			Team: Laker,
		}
		davis := &Player{
			Name: "Anthony Davis",
			Team: Laker,
		}
		l.Players = append(l.Players, james, davis)
		return l
	default:
		fmt.Printf("Get an invalid team id %v.\n", id)
		return nil}}Copy the code

The test code is as follows:

package test
...
func TestFlyweight(t *testing.T) {
	game1 := &nba.Match{
		Date:         time.Date(2020.1.10.9.30.0.0, time.Local),
		LocalTeam:    nba.Factory().TeamOf(nba.Warrior),
		VisitorTeam:  nba.Factory().TeamOf(nba.Laker),
		LocalScore:   102,
		VisitorScore: 99,
	}
	game1.ShowResult()
	game2 := &nba.Match{
		Date:         time.Date(2020.1.12.9.30.0.0, time.Local),
		LocalTeam:    nba.Factory().TeamOf(nba.Laker),
		VisitorTeam:  nba.Factory().TeamOf(nba.Warrior),
		LocalScore:   110,
		VisitorScore: 118,
	}
	game2.ShowResult()
  // Two matches of the same team should be the same instance
	ifgame1.LocalTeam ! = game2.VisitorTeam { t.Errorf("Warrior team do not use flyweight pattern")}}// Run the result
=== RUN   TestFlyweight
Golden State Warriors VS Los Angeles Lakers - 102:99
Los Angeles Lakers VS Golden State Warriors - 110:118
--- PASS: TestFlyweight (0.00s)
Copy the code

conclusion

In this paper, we mainly introduce the agent mode, decoration mode, appearance mode and share mode of structural mode. The proxy pattern provides a proxy for an object to control access to that object, with emphasis on access control of ontology objects. Decorator mode can dynamically superimpose new behaviors on ontology objects, with emphasis on adding new functions to ontology objects. Appearance mode provides a higher level of external unified interface for the subsystem, which emphasizes stratification and decoupling. The share pattern reduces system resource consumption by sharing objects, emphasizing how the same state can be shared across multiple objects.

So far, all seven structural patterns have been introduced. In the next article, we will introduce the last category of design patterns — Behavioral patterns.