Abstract: this article, we mainly introduce the agency mode, decoration mode, appearance mode and flyweight mode in the structural mode.
This article is shared from the HUAWEI cloud community " coming soon, here are the Go language implementations of 23 design patterns (3) ", the original author: Yuan Runzi.
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. . This article will introduce several structural modes: agency mode, decoration mode, appearance mode and flyweight mode.
Proxy Pattern
Introduction
proxy mode provides a proxy for an object to control access to the object , it is a very high-usage design mode, even in real life, it is also very common, such as concert ticket scalpers. Suppose you need to watch a concert, but the tickets on the official website have been sold out, so you went to the scene and bought one at a high price through the scalper. In this example, the scalper is equivalent to the agent of concert tickets. In the case that tickets cannot be purchased through official channels, you have completed the goal through the agent.
From the concert ticket example, we can also see that the key to using the proxy mode is When the Client is inconvenient to directly access an object, it provides a proxy object to control the object's access . The client actually accesses the proxy object, and the proxy object forwards the client's request to the ontology object for processing.
In program design, the agent mode is also divided into several types:
1. Remote proxy (remote proxy), the remote proxy is suitable for the object that provides the service on the remote machine, the service cannot be used through ordinary function calls, and it needs to be completed by the remote proxy. Because it is not possible to directly access ontology objects, all remote proxy objects usually do not directly hold references to ontology objects, but instead hold the addresses of remote machines to access ontology objects through network protocols.
2. Virtual proxy (virtual proxy), there are often some heavyweight service objects in program design, if you keep holding the object instance will consume system resources, then you can delay the object through the virtual proxy initialization.
3. Protection proxy (protection proxy). The protection proxy is used to control access to ontology objects. It is often used in scenarios that require authorization verification for client access.
4. Cache proxy (cache proxy). The cache proxy mainly adds a layer of cache between the Client and the ontology object to speed up the access of ontology objects. It is common in the scene of connecting to the database.
5. Smart reference (smart reference), the smart reference provides additional actions for the access of the ontology object. The common implementation is the smart pointer in C++, which provides the counting function for the access of the object. When the count of the accessed object is 0 When destroying the object.
These types of agents have the same implementation principle. Below we will introduce the Go language implementation of remote agents.
Go implementation
Consider the message processing system output to data storage in a database, the interface of the database is as follows:
package db
...
// Key-Value数据库接口
type KvDb interface {
// 存储数据
// 其中reply为操作结果,存储成功为true,否则为false
// 当连接数据库失败时返回error,成功则返回nil
Save(record Record, reply *bool) error
// 根据key获取value,其中value通过函数参数中指针类型返回
// 当连接数据库失败时返回error,成功则返回nil
Get(key string, value *string) error
}
type Record struct {
Key string
Value string
}
The database is a Key-Value database that uses map to store data. The following is the server implementation of the database. db.Server implements the db.KvDb interface:
package db
...
// 数据库服务端实现
type Server struct {
// 采用map存储key-value数据
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
}
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. For scenarios where the service provider and the service user are not on the same machine, it is better to use a remote agent Nevertheless.
In remote agents, the most common implementation is remote procedure call (Remote Procedure Call, RPC ), which allows client applications to directly call server applications on a different machine just like calling local objects. Methods. In the Go language field, in addition to the famous gRPC , the Go standard library net/rpc package also provides an implementation of RPC. Below, we provide external database server capabilities through net/rpc:
package db
...
// 启动数据库,对外提供RPC接口进行数据库的访问
func Start() {
rpcServer := rpc.NewServer()
server := &Server{data: make(map[string]string)}
// 将数据库接口注册到RPC服务器上
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.")
}
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 the message processing system, it does not need and should not know the underlying details of the interaction between the remote agent and the database server, which can reduce the coupling between the systems. Therefore, the remote agent needs to implement db.KvDb:
package db
...
// 数据库服务端远程代理,实现db.KvDb接口
type Client struct {
// RPC客户端
cli *rpc.Client
}
func (c *Client) Save(record Record, reply *bool) error {
var ret bool
// 通过RPC调用服务端的接口
err := c.cli.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
// 通过RPC调用服务端的接口
err := c.cli.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}
}
As a remote agent, db.Client does not directly hold a reference to db.Server, but instead holds 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, call the db.Client remote agent, and store the message on the database.
In "Using Go to Implement GoF's 23 Design Patterns (2)", after we introduced the three life cycle methods Start, Stop, and Status for Plugin, each new plugin needs to implement these three methods. However, the logic of these three methods of most plug-ins is basically the same, which leads to a certain degree of code redundancy. What is a good solution to the problem of duplication of code? combination mode!
Next, we use the combination mode to extract this method into a new object LifeCycle, so that when adding a plug-in, you only need to use LifeCycle as an anonymous member ( embedded combination ) to solve the redundant code problem.
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
}
The implementation of DbOutput is as follows. It holds a remote agent through which messages are stored in a remote database.
package plugin
...
type DbOutput struct {
LifeCycle
// 操作数据库的远程代理
proxy 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"
}
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()
// 验证DbOutput存储的正确性
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.", val)
}
}
// 运行结果
=== 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
Decorator Pattern
Introduction
In program design, we often need to add new behaviors to objects. The first idea of many students is to extend the ontology object and achieve the goal through inheritance. However, the use of inheritance inevitably has the following two disadvantages: (1) Inheritance is static, which is determined during compilation and cannot change the behavior of the object at runtime. (2) Subclasses can only have one parent class. When too many new functions need to be added, the number of classes will easily increase.
For this kind of scene, we usually use decoration pattern (Decorator Pattern) to solve, uses composition instead of inheritance, which can dynamically superimpose new behavior for ontology objects. In theory, as long as there are no restrictions, it can always superimpose functions. The most classic application of the decoration mode is the Java I/O stream system. Through the decoration mode, users can dynamically add functions to the original input and output streams, such as input and output according to strings, adding cache, etc., to make the entire I/O The stream system has high scalability and flexibility.
From the structural point of view, the decoration mode and the agency mode have high similarities, but the two emphasized points are different. emphasizes the addition of new functions for ontology objects, while the latter emphasizes access control for ontology objects. Of course, the smart quotation in the proxy mode is exactly the same as the decoration mode in the author's opinion.
Go implementation
Consider adding such a function to the message processing system to count how many messages are generated by each message input source, that is, to count the number of messages generated by each Input. The simplest method is to do some statistics in the Receive method of each Input, but this will cause the coupling of the statistical code and the business code. If the statistical logic changes, shotgun modification will occur. With the increase of Input types, the related code will become more and more difficult to maintain.
A better way is to put the statistical logic in one place, and do some statistics after each call to the Receive method of Input. And this is just suitable for the use of decoration mode, for Input ( ontology object ) to provide the dot statistics function ( new behavior ). We can design an InputMetricDecorator as the decorator of Input, and complete the logic of statistics management in the decorator.
First, we need to design an object for counting the number of Messages generated by each Input. This object should be globally unique, so it is implemented in a singleton mode:
package metric
...
// 消息输入源统计,设计为单例
type input struct {
// 存放统计结果,key为Input类型如hello、kafka
// value为对应Input的消息统计
metrics map[string]uint64
// 统计打点时加锁
mu *sync.Mutex
}
// 给名称为inputName的Input消息计数加1
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
}
// 输出当前所有打点的情况
func (i *input) Show() {
fmt.Printf("Input metric: %v\n", i.metrics)
}
// 单例
var inputInstance = &input{
metrics: make(map[string]uint64),
mu: &sync.Mutex{},
}
func Input() *input {
return inputInstance
}
Next we start to implement InputMetricDecorator, which implements the Input interface and holds an ontology object Input. In the InputMetricDecorator, call the Receive method of the ontology Input in the Receive method, and complete the statistical action.
package plugin...type InputMetricDecorator struct { input Input}func (i *InputMetricDecorator) Receive() *msg.Message { // 调用本体对象的Receive方法 record := i.input.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.input.Status()}func (i *InputMetricDecorator) Init() { i.input.Init()}// 工厂方法, 完成装饰器的创建func CreateInputMetricDecorator(input Input) *InputMetricDecorator { return &InputMetricDecorator{input: input}}
Finally, we add the InputMetricDecorator proxy to the ontology Input on the factory method of the Pipeline:
package 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) // 为本体Input加上InputMetricDecorator装饰器 p.input = plugin.CreateInputMetricDecorator(p.input) return p}
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 TestInputMetricDecoratorConsole 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
Facade Pattern
Introduction
From the structural point of view, the appearance model is very simple. It is mainly that provides a higher-level external unified interface for the subsystem, so that the Client can use the subsystem's function more friendly. In the figure, Subsystem Class is the abbreviation of the object in the subsystem. It may be an object or a collection of dozens of objects. The appearance mode reduces the coupling between the Client and the Subsystem. As long as the facade remains the same, no matter how the Subsystem changes, it is insensible to the Client.
Appearance mode is used a lot in program design. For example, when we click the button to buy on the mall, for buyers, they only see the unified interface of purchase, but for the mall system, there is an internal process. Series of business processing, such as inventory checking, order processing, payment, logistics, etc. The appearance model greatly improves the user experience and frees users from complex business processes.
The appearance mode is often applied to the layered architecture , usually we will provide one or more unified external access interfaces for each level in the layered architecture, so that the coupling between the various levels can be lower, so that The structure of the system is more reasonable.
Go implementation
The appearance model is also very simple to implement, so consider the previous message processing system. In the Pipeline, each message will be by 160e3d548e39f1 Input->Filter->Output in turn. The code implementation is like this:
p := pipeline.Of(config)message := p.input.Receive()message = p.filter.Process(message)p.output.Send(message)
However, for a Pipeline user, he may not care about the specific processing flow of the message, he only needs to know 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)}
In this way, the user can complete a message processing by simply calling the Exec method. The test code is as follows:
package test...func TestPipeline(t *testing.T) { p := pipeline.Of(pipeline.HelloConfig()) p.Start() // 调用Exec方法完成一次消息的处理 p.Exec()}// 运行结果=== RUN TestPipelineconsole 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
Flyweight Pattern
Introduction
In programming, we often encounter some very heavy objects, which usually have many member attributes. When the system is flooded with a large number of these objects, the system's memory will be under tremendous pressure. In addition, the frequent creation of these objects also greatly consumes the system's CPU. Many times, in these heavy objects, most of the member attributes are fixed. In this scenario, you can use mode to optimize, and design the fixed part as a shared object (flyweight) , So you can save a lot of system memory and CPU.
mode abandons the way of storing all data in each object, and allows you to load more objects in a limited memory capacity by sharing the same state shared by multiple objects.
When we decide to optimize a heavy object using the flyweight model, we first need to divide the attributes of the heavy object into two categories, the ones that can be shared and those that cannot be shared. The former is called internal state (intrinsic state), which is stored in the ; the latter is called 160e3d548e3b6b external state (extrinsic state), and its value depends on the flyweight The context in which it is located, so it cannot be shared. For example, both article A and article B refer to image A. Since the text content of article A and article B are different, the text is an external state and cannot be shared; but the image A cited by them is the same and belongs to an internal state , So picture A can be designed as a Flyweight
mode. The Flyweight factory provides the only interface to obtain the Flyweight object, so that the Client does not perceive how the Flyweight is shared, which reduces the coupling of the module. mode and 160e3d548e3b9e singleton mode are similar in that they share objects in the system, but the singleton mode is more concerned about objects are created only once in the system , while Flyweight mode is more concerned about how Share the same state among multiple objects.
Go implementation
Suppose you now need to design a system to record player information, team information, and game results in the NBA.
The data structure of the team is defined as follows:
package nba
...
type TeamId uint8
const (
Warrior TeamId = iota
Laker
)
type Team struct {
Id TeamId // 球队ID
Name string // 球队名称
Players []*Player // 球队中的球员
}
The data structure of Player is defined as follows:
package nba
...
type Player struct {
Name string // 球员名字
Team TeamId // 球员所属球队ID
}
The data structure of the match result Match is defined as follows:
package nba
...
type Match struct {
Date time.Time // 比赛时间
LocalTeam *Team // 主场球队
VisitorTeam *Team // 客场球队
LocalScore uint8 // 主场球队得分
VisitorScore uint8 // 客场球队得分
}
func (m *Match) ShowResult() {
fmt.Printf("%s VS %s - %d:%d\n", m.LocalTeam.Name, m.VisitorTeam.Name,
m.LocalScore, m.VisitorScore)
}
A game in the NBA consists of two teams, the home team and the away team, to complete the game. The corresponding code is, a Match instance will hold 2 Team instances. At present, the NBA consists of a total of 30 teams. According to each team playing 82 regular season games in each season, there will be a total of 2,460 games in a season. Correspondingly, there will be 4,920 Team instances. However, the 30 teams in the NBA are fixed. In fact, only 30 Team instances can completely record all game information for a season. The remaining 4890 Team instances are redundant data.
In this scenario, Flyweight mode is suitable for optimization. We design Team as a Flyweight between multiple Match instances. The acquisition of Flyweight is done through the Flyweight Factory. The Flyweight Factory teamFactory is defined as follows. The Client uses the teamFactory.TeamOf method to obtain the team instance. Among them, each team Team instance will only be created once, and then added to the team pool, subsequent acquisitions are obtained directly from the pool, so that the purpose of sharing is achieved.
package nba
...
type teamFactory struct {
// 球队池,缓存球队实例
teams map[TeamId]*Team
}
// 根据TeamId获取Team实例,从池中获取,如果池里没有,则创建
func (t *teamFactory) TeamOf(id TeamId) *Team {
team, ok := t.teams[id]
if !ok {
team = createTeam(id)
t.teams[id] = team
}
return team
}
// 享元工厂的单例
var factory = &teamFactory{
teams: make(map[TeamId]*Team),
}
func Factory() *teamFactory {
return factory
}
// 根据TeamId创建Team实例,只在TeamOf方法中调用,外部不可见
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
}
}
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()
// 两个Match的同一个球队应该是同一个实例的
if game1.LocalTeam != game2.VisitorTeam {
t.Errorf("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.00s)
to sum up
In this article, we mainly introduce the agency model, decoration model, appearance model and flyweight model in the structural model. proxy mode provides a proxy for an object to control access to the object, emphasizing access control to the ontology object; decoration mode can dynamically superimpose new behaviors for ontology objects, and the emphasis is on ontology objects Add new functions; appearance mode provides a higher level of external unified interface for the subsystem, emphasizing layering and decoupling; mode reduces system resource consumption by sharing objects, emphasizing that How to share the same state among multiple objects.
Click to follow and learn about Huawei Cloud's fresh technology for the first time~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。