Abstract: In this paper, we mainly introduce the agent mode, decoration mode, appearance mode and share mode of structural mode.
This article is shared by Huawei Cloud community “Come, here are 23 design modes of Go language implementation (3)”, the original author: Yuan Runzi.
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. This article introduces several structural patterns: the proxy pattern, the decorator pattern, the facade pattern, and the share pattern.
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 ... Type KvDb interface {// Store data // reply indicates the operation result, true if the operation succeeds, false otherwise // Error is returned when the database fails to connect, Return nil Save(record record, reply *bool) error 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 ... 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 ... Func Start() {rpcServer := rpc.newServer () server := & server {data: Make (map[string]string)} // Register the database interface with the RPC server if err := 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") if err! = 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, Type Client struct {// RPC Client cli *rpc.Client} func (c *Client) Save(record record, Reply *bool) error {var ret bool // Interface used to Call the Server through RPC err := c. C. Call(" server. Save", record, &ret) if err! = 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 Server interface through RPC err := c. C. Call(" server. Get", key, &ret) if err! = nil { fmt.Printf("Call db Server.Get rpc failed, error: %v", err) *reply = "" return err} *reply = ret return nil} Func CreateClient() *Client {rpcCli, err := rpc.Dial(" TCP ", "127.0.0.1:1234") if err! = 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() {
l.status = Started
fmt.Printf("%s plugin started.\n", l.name)
}
func (l *LifeCycle) Stop() {
l.status = Stopped
fmt.Printf("%s plugin stopped.\n", l.name)
}
func (l *LifeCycle) Status() 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 ... Db.kvdb} func (d *DbOutput) Send(MSG * msg.message) {if d.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) if err ! = nil || ! reply { fmt.Println("Save msg to db server failed.") } } func (d *DbOutput) Init() { 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.ec () // verify that DbOutput is correct cli := db.createclient () var val string err := cli.Get("db", &val) if err ! = nil { t.Errorf("Get db failed, error: %v\n.", err) } if val ! = "HELLO WORLD" { t.Errorf("expect HELLO WORLD, but actual %s.", === RUN TestDbOutput Rpc server start success. Db output plugin started. Upper filter plugin started. Pipeline started. -- PASS: TestDbOutput (1.01s) PASSCopy 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 ... Type input struct {// Metrics Map [STRING]uint64 // Mu *sync.Mutex} // statistics are locked when statistics are generated Add 1 func (I * Input) Inc(inputName string) {i.mlock () defer i.MLlock () if _, ok := i.metrics[inputName]; ! Ok {i.metrics[inputName] = 0} i.metrics[inputName] = i.metrics[inputName] + 1} Printf("Input metric: %v\n", i.metrics)} var inputInstance = & Input {metrics: make(map[string]uint64), mu: &sync.Mutex{}, } func Input() *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() *msg.Message { // Receive() if inputName, ok := record.header. Items["input"]; ok { metric.Input().Inc(inputName) } return record}func (i *InputMetricDecorator) Start() { i.input.Start()}func (i *InputMetricDecorator) Stop() { i.input.Stop()}func (i *InputMetricDecorator) Status() Status { return I.put.status ()}func (I *InputMetricDecorator) Init() {i.put.init ()} Complete the decorators create 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... 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 InputMetricDecorator p.input= to ontology 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()}// Results = = = RUN TestInputMetricDecoratorConsole output plugins started. The 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: PASS TestInputMetricProxy (0.00 s)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, SubsystemClass is short for an object in a subsystem. It can 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() { 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... T) {p := pipeline.of (pipeline.helloConfig ()) p.start () // call the Exec method to complete the processing Of a message === RUN TestPipelineconsole output plugin started. Upper filter plugin started. Hello input plugin started Started.Output: Header:map[content:text input:hello], Body:[Hello WORLD]-- PASS: TestPipeline (0.00s)PASSCopy 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 // TeamId Name string // Team Name Players []*Player // team Player}Copy the code
The Player data structure is defined as follows:
package nba ... Type Player struct {Name string Team TeamId // Team ID}Copy the code
The data structure of Match result is defined as follows:
package nba ... // Uint8 LocalTeam *Team // Home Team *Team // Home Team *Team VisitorScore // uint8 uint8 // away team score} func (m *Match) 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 ... TeamFactory struct {// Team map[TeamId]*Team} // Team map[TeamId]*Team} Create func (t *teamFactory) TeamOf(id TeamId) *Team {Team, ok := t.ams [id] if! Ok {team = createTeam(id) t.ams [id] = team} return team} var factory = &teamFactory{teams: Make (map[TeamId]*Team),} func Factory() *teamFactory {return Factory} 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 of if game1.localteam! Game2. VisitorTeam {t. rorf("Warrior Team do not use flyweight pattern")}} // RUN TestFlyweight Golden State Warriors VS Los Angeles Lakers - 102:99 Los Angeles Lakers VS Golden State Warriors - 110:118 --- PASS: TestFlyweight (0.00 s)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.
Click to follow, the first time to learn about Huawei cloud fresh technology ~