乐趣区

关于设计模式:带你认识4种设计模式代理模式装饰模式外观模式和享元模式

摘要:本文咱们次要介绍结构型模式中的代理模式、装璜模式、外观模式和享元模式。

本文分享自华为云社区《快来,这里有 23 种设计模式的 Go 语言实现(三)》,原文作者:元闰子。

设计模式(Design Pattern)是一套被重复应用、少数人通晓的、通过分类编目标、代码设计教训的总结,应用设计模式是为了可重用代码、让代码更容易被别人了解并且保障代码可靠性。本文将介绍几种结构型模式:代理模式、装璜模式、外观模式和享元模式。

代理模式(Proxy Pattern)

简介

代理模式为一个对象提供一种代理以管制对该对象的拜访,它是一个使用率十分高的设计模式,即便在现实生活中,也是很常见,比方演唱会门票黄牛。假如你须要看一场演唱会,然而官网上门票曾经售罄,于是就当天到现场通过黄牛高价买了一张。在这个例子中,黄牛就相当于演唱会门票的代理,在正式渠道无奈购买门票的状况下,你通过代理实现了该指标。

从演唱会门票的例子咱们也能够看出,应用代理模式的关键在于 当 Client 不不便间接拜访一个对象时,提供一个代理对象管制该对象的拜访。Client 实际上拜访的是代理对象,代理对象会将 Client 的申请转给本体对象去解决。

在程序设计中,代理模式也分为好几种:

1、近程代理(remote proxy),近程代理实用于提供服务的对象处在近程的机器上,通过一般的函数调用无奈应用服务,须要通过近程代理来实现。因为并不能间接拜访本体对象,所有近程代理对象通常不会间接持有本体对象的援用,而是持有远端机器的地址,通过网络协议去拜访本体对象。

2、虚构代理(virtual proxy),在程序设计中经常会有一些重量级的服务对象,如果始终持有该对象实例会十分耗费系统资源,这时能够通过虚构代理来对该对象进行提早初始化。

3、爱护代理(protection proxy),爱护代理用于管制对本体对象的拜访,罕用于须要给 Client 的拜访加上权限验证的场景。

4、缓存代理(cache proxy),缓存代理次要在 Client 与本体对象之间加上一层缓存,用于减速本体对象的拜访,常见于连贯数据库的场景。

5、智能援用(smart reference),智能援用为本体对象的拜访提供了额定的动作,常见的实现为 C ++ 中的智能指针,为对象的拜访提供了计数性能,当拜访对象的计数为 0 时销毁该对象。

这几种代理都是一样的实现原理,上面咱们将介绍近程代理的 Go 语言实现。

Go 实现

思考要将音讯解决零碎输入到数据存储到一个数据库中,数据库的接口如下:

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
}

数据库是一个 Key-Value 数据库,应用 map 存储数据,上面为数据库的服务端实现,db.Server 实现了 db.KvDb 接口:

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
}

音讯解决零碎和数据库并不在同一台机器上,因而音讯解决零碎不能间接调用 db.Server 的办法进行数据存储,像这种服务提供者和服务使用者不在同一机器上的场景,应用近程代理再适宜不过了。

近程代理中,最常见的一种实现是 近程过程调用(Remote Procedure Call,简称 RPC),它容许客户端利用能够像调用本地对象一样间接调用另一台不同的机器上服务端利用的办法。在 Go 语言畛域,除了赫赫有名的gRPC,Go 规范库 net/rpc 包里也提供了 RPC 的实现。上面,咱们通过 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.")
}

到目前为止,咱们曾经为数据库提供了对外拜访的形式。当初,咱们须要一个近程代理来连贯数据库服务端,并进行相干的数据库操作。对音讯解决零碎而言,它不须要,也不应该晓得近程代理与数据库服务端交互的底层细节,这样能够加重零碎之间的耦合。因而,近程代理须要实现 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}
}

作为近程代理的 db.Client 并没有间接持有 db.Server 的援用,而是持有了它的 ip:port,通过 RPC 客户端调用了它的办法。

接下来,咱们须要为音讯解决零碎实现一个新的 Output 插件 DbOutput,调用 db.Client 近程代理,将音讯存储到数据库上。

在《应用 Go 实现 GoF 的 23 种设计模式(二)》中咱们为 Plugin 引入生命周期的三个办法 Start、Stop、Status 之后,每新增一个新的插件,都须要实现这三个办法。然而大多数插件的这三个办法的逻辑基本一致,因而导致了肯定水平的代码冗余。对于反复代码问题,有什么好的解决办法呢?组合模式!

上面,咱们应用组合模式将这个办法提取成一个新的对象 LifeCycle,这样新增一个插件时,只需将 LifeCycle 作为匿名成员(嵌入组合),就能解决冗余代码问题了。

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}

DbOutput 的实现如下,它持有一个近程代理,通过后者将音讯存储到远端的数据库中。

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"
}

测试代码如下:

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)

简介

在程序设计中,咱们经常须要为对象增加新的行为,很多同学的第一个想法就是扩大本体对象,通过继承的形式达到目标。然而应用继承不可避免地有如下两个弊病:(1)继承时动态的,在编译期间就曾经确定,无奈在运行时扭转对象的行为。(2)子类只能有一个父类,当须要增加的新性能太多时,容易导致类的数量剧增。

对于这种场景,咱们通常会应用 装璜模式 (Decorator Pattern)来解决, 它应用组合而非继承的形式,可能动静地为本体对象叠加新的行为。实践上,只有没有限度,它能够始终把性能叠加上来。装璜模式最经典的利用当属 Java 的 I / O 流体系,通过装璜模式,使用者能够动静地为原始的输入输出流增加性能,比方依照字符串输入输出,增加缓存等,使得整个 I / O 流体系具备很高的可扩展性和灵活性。

从构造上看,装璜模式和代理模式具备很高的相似性,然而两种所强调的点不一样。前者强调的是为本体对象增加新的性能,后者强调的是对本体对象的访问控制。当然,代理模式中的智能援用在笔者看来就跟装璜模式齐全一样了。

Go 实现

思考为音讯解决零碎减少这样的一个性能,统计每个音讯输出源别离产生了多少条音讯,也就是别离统计每个 Input 产生 Message 的数量。最简略的办法是在每一个 Input 的 Receive 办法中进行打点统计,然而这样会导致统计代码与业务代码的耦合。如果统计逻辑产生了变动,就会产生 霰弹式批改,随着 Input 类型的增多,相干代码也会变得越来越难保护。

更好的办法是将统计逻辑放到一个中央,并在每次调用 Input 的 Receive 办法后进行打点统计。而这恰好适宜采纳装璜模式,为 Input(本体对象 )提供打点统计性能( 新的行为)。咱们能够设计一个 InputMetricDecorator 作为 Input 的装璜器,在装璜器中实现打点统计的逻辑。

首先,咱们须要设计一个用于统计每个 Input 产生 Message 数量的对象,该对象应该是一个全局惟一的,因而采纳单例模式进行了实现:

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}

接下来咱们开始实现 InputMetricDecorator,它实现了 Input 接口,并持有一个本体对象 Input。在 InputMetricDecorator 在 Receive 办法中调用本体 Input 的 Receive 办法,并实现统计动作。

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}}

最初,咱们在 Pipeline 的工厂办法上,为本体 Input 加上 InputMetricDecorator 代理:

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}

测试代码如下:

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)

简介

从构造上看,外观模式十分的简略,它次要是 为子系统提供了一个更高层次的对外对立接口,使得 Client 可能更敌对地应用子系统的性能。图中,Subsystem Class 是子系统中对象的简称,它可能是一个对象,也可能是数十个对象的汇合。外观模式升高了 Client 与 Subsystem 之间的耦合,只有 Facade 不变,不论 Subsystem 怎么变动,对于 Client 而言都是无感知的。

外观模式在程序设计中用的十分多,比方咱们在商城上点击购买的按钮,对于购买者而言,只看到了购买这一对立的接口,然而对于商城零碎而言,其外部则进行了一系列的业务解决,比方库存查看、订单解决、领取、物流等等。外观模式极大地晋升了用户体验,将用户从简单的业务流程中解放了进去。

外观模式常常使用于 分层架构 上,通常咱们都会为分层架构中的每一个层级提供一个或多个对立对外的拜访接口,这样就能让各个层级之间的耦合性更低,使得零碎的架构更加正当。

Go 实现

外观模式实现起来也很简略,还是思考后面的音讯解决零碎。在 Pipeline 中,每一条音讯会顺次通过 Input->Filter->Output 的解决,代码实现起来就是这样:

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

然而,对于 Pipeline 的使用者而言,他可能并不关怀音讯具体的解决流程,他只需晓得音讯曾经通过 Pipeline 解决即可。因而,咱们须要设计一个简略的对外接口:

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

这样,使用者只需简略地调用 Exec 办法,就能实现一次音讯的解决,测试代码如下:

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)

简介

在程序设计中,咱们经常会碰到一些很重型的对象,它们通常领有很多的成员属性,当零碎中充斥着大量的这些对象时,零碎的内存将会接受微小的压力。此外,频繁的创立这些对象也极大地耗费了零碎的 CPU。很多时候,这些重型对象里,大部分的成员属性都是固定的,这种场景下,能够应用 享元模式 进行优化,将其中固定不变的局部设计成共享对象(享元,flyweight),这样就能节俭大量的零碎内存和 CPU。

享元模式摒弃了在每个对象中保留所有数据的形式,通过共享多个对象所共有的雷同状态,让你能在无限的内存容量中载入更多对象。

当咱们决定对一个重型对象采纳享元模式进行优化时,首先须要将该重型对象的属性划分为两类,可能共享的和不能共享的。前者咱们称为 外部状态 (intrinsic state),存储在享元中,不随享元所处上下文的变动而变动;后者称为 内部状态(extrinsic state),它的值取决于享元所处的上下文,因而不能共享。比方,文章 A 和文章 B 都援用了图片 A,因为文章 A 和文章 B 的文字内容是不一样的,因而文字就是内部状态,不能共享;然而它们所援用的图片 A 是一样的,属于外部状态,因而能够将图片 A 设计为一个享元

工厂模式 通常都会和享元模式结对呈现,享元工厂提供了惟一获取享元对象的接口,这样 Client 就感知不到享元是如何共享的,升高了模块的耦合性。享元模式和 单例模式 有些相似的中央,都是在零碎中共享对象,然而单例模式更关怀的是 对象在零碎中仅仅创立一次 ,而享元模式更关怀的是 如何在多个对象中共享雷同的状态。

Go 实现

假如当初须要设计一个零碎,用于记录 NBA 中的球员信息、球队信息以及比赛结果。

球队 Team 的数据结构定义如下:

package nba
...
type TeamId uint8

const (
    Warrior TeamId = iota
    Laker
)

type Team struct {
    Id      TeamId    // 球队 ID
    Name    string    // 球队名称
    Players []*Player // 球队中的球员}

球员 Player 的数据结构定义如下:

package nba
...
type Player struct {
    Name string // 球员名字
    Team TeamId // 球员所属球队 ID
}

比赛结果 Match 的数据结构定义如下:

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)
}

NBA 中的一场较量由两个球队,主场球队和客场球队,实现较量,对应着代码就是,一个 Match 实例会持有 2 个 Team 实例。目前,NBA 总共由 30 支球队,依照每个赛季每个球队打 82 场常规赛算,一个赛季总共会有 2460 场较量,对应地,就会有 4920 个 Team 实例。然而,NBA 的 30 支球队是固定的,实际上只需 30 个 Team 实例就能残缺地记录一个赛季的所有较量信息,剩下的 4890 个 Team 实例属于冗余的数据。

这种场景下就适宜采纳享元模式来进行优化,咱们把 Team 设计成多个 Match 实例之间的享元。享元的获取通过享元工厂来实现,享元工厂 teamFactory 的定义如下,Client 对立应用 teamFactory.TeamOf 办法来获取球队 Team 实例。其中,每个球队 Team 实例只会创立一次,而后增加到球队池中,后续获取都是间接从池中获取,这样就达到了共享的目标。

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
    }
}

测试代码如下:

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)

总结

本文咱们次要介绍了结构型模式中的代理模式、装璜模式、外观模式和享元模式。代理模式 为一个对象提供一种代理以管制对该对象的拜访,强调的是对本体对象的访问控制;装璜模式 可能动静地为本体对象叠加新的行为,强调的是为本体对象增加新的性能;外观模式 为子系统提供了一个更高层次的对外对立接口,强调的是分层和解耦;享元模式 通过共享对象来升高零碎的资源耗费,强调的是如何在多个对象中共享雷同的状态。

点击关注,第一工夫理解华为云陈腐技术~

退出移动版