结构型模式

结构型模式描述如何将类或者对象结合在一起形成更大的结构。

0.1. 外观模式

0.1.1. 定义

外观模式(Facade Pattern):外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。外观模式是一种对象结构型模式。

外观模式包含如下角色:

  • Facade: 外观对象
  • SubSystem:子系统对象

image

image

0.1.2. 样例

样例代码来源

子系统对象A:

package facade

type AModuleAPI interface {
    TestA() string
}

type aModuleImpl struct {
}

func NewAModuleImpl() AModuleAPI {
    return &aModuleImpl{}
}

func (a *aModuleImpl) TestA() string {
    return "A module running"
}

子系统对象B:

package facade

type BModuleAPI interface {
    TestB() string
}

type bModuleImpl struct {
}

func NewBModuleImpl() BModuleAPI {
    return &bModuleImpl{}
}

func (b *bModuleImpl) TestB() string {
    return "B Module running"
}

外观对象:

package facade

import "fmt"

type API interface {
    Test() string
}

type apiImpl struct {
    a AModuleAPI
    b BModuleAPI
}

func NewApiImpl(a AModuleAPI, b BModuleAPI) *apiImpl {
    return &apiImpl{
        a: NewAModuleImpl(),
        b: NewBModuleImpl(),
    }
}

func (a *apiImpl) Test() string {
    aRet := a.a.TestA()
    bRet := a.b.TestB()
    return fmt.Sprintf("%s\n%s", aRet, bRet)
}

API为facade模块的外观接口,大部分代码使用此接口简化对facade类的访问。

facade模块同时暴露了a和b 两个Module 的NewXXX和interface,其它代码如果需要使用细节功能时可以直接调用。

0.1.3. 分析

根据“单一职责原则”,在软件中将一个系统划分为若干个子系统有利于降低整个系统的复杂性,一个常见的设计目标是使子系统间的通信和相互依赖关系达到最小,而达到该目标的途径之一就是引入一个外观对象,它为子系统的访问提供了一个简单而单一的入口。

  • 外观模式:也是“迪米特法则”的体现,通过引入一个新的外观类可以降低原有系统的复杂度,同时降低客户类与子系统类的耦合度。
  • 外观模式:要求一个子系统的外部与其内部的通信通过一个统一的外观对象进行,外观类将客户端与子系统的内部复杂性分隔开,使得客户端只需要与外观对象打交道,而不需要与子系统内部的很多对象打交道。
  • 外观模式:目的在于降低系统的复杂程度。
  • 外观模式:从很大程度上提高了客户端使用的便捷性,使得客户端无须关心子系统的工作细节,通过外观对象即可调用相关功能。

更详细的介绍看图解设计模式

0.2. 适配器模式

0.2.1. 定义

适配器模式(Adapter Pattern) :将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。

适配器模式包含如下角色:

  • Target:目标抽象类
  • Adapter:适配器类
  • Adaptee:被适配类
  • Client:客户类

image

image

0.2.2. 样例

样例代码来源

目标抽象类:

package adapter

type Target interface {
    Request() string
}

被适配类:

package adapter

type Adaptee interface {
    SpecificRequest() string
}

type adapteeImpl struct {
}

func NewAdaptee() Adaptee {
    return &adapteeImpl{}
}

func (a *adapteeImpl) SpecificRequest() string {
    return "adaptee method"
}

适配器类:

package adapter

type adapter struct {
    Adaptee
}

func NewAdapter(adaptee Adaptee) Target {
    return &adapter{Adaptee: adaptee}
}

func (a *adapter) Request() string {
    return a.SpecificRequest()
}

实际使用中Adaptee一般为接口,并且使用工厂函数生成实例。

在Adapter中匿名组合Adaptee接口,所以Adapter类也拥有SpecificRequest实例方法,又因为Go语言中非入侵式接口特征,其实Adapter也适配Adaptee接口。

0.2.3. 分析

  • 适配器模式:用于将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器。
  • 适配器模式:既可以作为类结构型模式,也可以作为对象结构型模式。

适配器模式包含四个角色:

  1. 目标抽象类:定义客户要用的特定领域的接口;
  2. 适配器类:可以调用另一个接口,作为一个转换器,对适配者和抽象目标类进行适配,它是适配器模式的核心;
  3. 被适配类:是被适配的角色,它定义了一个已经存在的接口,这个接口需要适配;
  4. 在客户类中针对目标抽象类进行编程,调用在目标抽象类中定义的业务方法。

在类适配器模式中,适配器类实现了目标抽象类接口并继承了适配者类,并在目标抽象类的实现方法中调用所继承的适配者类的方法;

在对象适配器模式中,适配器类继承了目标抽象类并定义了一个适配者类的对象实例,在所继承的目标抽象类方法中调用适配者类的相应业务方法。

适配器模式的主要优点是将目标类和被适配类解耦,增加了类的透明性和复用性,同时系统的灵活性和扩展性都非常好,更换适配器或者增加新的适配器都非常方便,符合“开闭原则”;

适配器模式适用情况包括:

  1. 系统需要使用现有的类,而这些类的接口不符合系统的需要;
  2. 想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类一起工作。

更详细的介绍看图解设计模式

0.3. 装饰模式

0.3.1. 定义

装饰模式(Decorator Pattern) :

  • 动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类实现更为灵活。
  • 其别名也可以称为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合,装饰模式是对功能的增强,这是它应用场景的一个重要特点。
  • 根据翻译的不同,装饰模式也有人称之为“油漆工模式”,它是一种对象结构型模式。

装饰模式包含如下角色:

  • Component: 抽象构件(type a interface
  • ConcreteComponent: 具体构件 (a接口的实现)
  • Decorator: 抽象装饰类(type b interface
  • ConcreteDecorator: 具体装饰类 (ab接口的实现,将a组合到结构体中)

修饰类与原始类继承同样的父类,这样可以对原始类嵌套多个修饰类。

image

image

0.3.3. 样例

样例代码来源

抽象构件:(父类)

package decorator

type Component interface {
    Calc() int
}

具体构件:(原始类)

package decorator

type ConcreteComponent struct {
}

func (c *ConcreteComponent) Calc() int {
    return 0
}

具体装饰类:(修饰类的实现)

package decorator

type MullDecorator struct {
    Component
    num int
}

func WarpMullDecorator(c Component, num int) Component {
    return &MullDecorator{
        Component: c,
        num:       num}
}

func (d *MullDecorator) Calc() int {
    return d.Component.Calc() * d.num
}

type AddDecorator struct {
    Component
    num int
}

func WarpAddDecorator(c Component, num int) Component {
    return &AddDecorator{
        Component: c,
        num:       num,
    }
}

func (d *AddDecorator) Calc() int {
    return d.Component.Calc() + d.num
}

装饰模式使用对象组合的方式动态改变或增加对象行为。

Go语言借助于匿名组合和非入侵式接口可以很方便实现装饰模式。

使用匿名组合,在装饰器中不必显式定义转调原对象方法。

0.3.4. 分析

装饰模式用于动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。它是一种对象结构型模式。

装饰模式包含四个角色:

  1. 抽象构件:定义了对象的接口,可以给这些对象动态增加职责(方法);
  2. 具体构件:定义了具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责(方法);
  3. 抽象装饰类:抽象构件类的子类,用于给具体构件增加职责,但是具体职责在其子类中实现;
  4. 具体装饰类:抽象装饰类的子类,负责向构件添加新的职责。

使用装饰模式来实现扩展比继承更加灵活,它以对客户透明的方式动态地给一个对象附加更多的责任。

装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。

装饰模式的主要优点在于可以提供比继承更多的灵活性,可以通过一种动态的方式来扩展一个对象的功能,并通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合,而且具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类;

主要缺点在于使用装饰模式进行系统设计时将产生很多小对象,而且装饰模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。

装饰模式适用情况包括:

  1. 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责;
  2. 需要动态地给一个对象增加功能,这些功能也可以动态地被撤销;
  3. 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。

装饰模式可分为透明装饰模式和半透明装饰模式:

  • 在透明装饰模式中,要求客户端完全针对抽象编程,装饰模式的透明性要求客户端程序不应该声明具体构件类型和具体装饰类型,而应该全部声明为抽象构件类型;
  • 半透明装饰模式中,允许用户在客户端声明具体装饰者类型的对象,调用在具体装饰者中新增的方法。

更详细的介绍看图解设计模式

上次修改: 29 April 2020