结构型模式

寒江蓑笠翁大约 24 分钟设计模式设计模式go

结构型模式

结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式, 前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。 由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型 模式具有更大的灵活性。

代理模式

由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。

  • 抽象主题(Subject)接口: 通过接口或抽象类声明真实主题和代理对象实现的业务方法。

  • 真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。

  • 代理(Proxy)类 :提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问,控制或扩展真实主题的功能。

// 抽象主题
type Subject interface {
	Do()
}

// 具体主题
type RealSubject struct {
}

func (r RealSubject) Do() {
	fmt.Println("do something")
}

// 代理
type ProxySubject struct {
	sub Subject
}

func (p ProxySubject) Do() {
	fmt.Println("before")
	p.sub.Do()
	fmt.Println("after")
}

func NewProxy() ProxySubject {
	return ProxySubject{sub: RealSubject{}}
}

func TestProxy(t *testing.T) {
	proxy := NewProxy()
	proxy.Do()
}

上述这种代理方式是静态代理,对Java有过了解的人可能会想着在Go中搞动态代理,但显然这是不可能的。要知道动态代理的核心是反射,Go确实支持反射,但不要忘了一点是Go是纯粹的静态编译型语言,而Java看似是一个编译型语言,但其实是一个动态的解释型语言,JDK动态代理就是在运行时生成字节码然后通过类加载器加载进JVM的,这对于Go来讲是完全不可能的事情。

适配器模式

将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。 适配器模式分为类适配器模式和对象适配器模式,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。

  • 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
  • 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
  • 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。

举个例子,现在有一个苹果手机,它只支持苹果充电器,但是手上只有一个安卓充电器,这时候需要一个适配器让安卓充电器也可以给苹果手机充电。

适配模式分类适配和对象适配,区别在于前者在适配原有组件时使用的是继承,而后者是组合,通常建议用后者。

// 苹果充电器
type IphoneCharge interface {
	IosCharge() string
}

// 安卓充电器
type AndroidCharge struct {
}

// 安卓可以Type-c充电
func (a AndroidCharge) TypeCCharge() string {
	return "type-c charge"
}

// 适配器
type Adapter struct {
	android AndroidCharge
}

// 适配器组合了安卓充电器,又实现了苹果充电器的接口
func (a *Adapter) IosCharge() string {
	fmt.Println(a.android.TypeCCharge())
	fmt.Println("type-c to ios charge")
	return "iphone charge"
}

func TestAdapter(t *testing.T) {
	var ios IphoneCharge
	ios = &Adapter{AndroidCharge{}}
	fmt.Println(ios.IosCharge())
}

装饰模式

装饰模式又名包装(Wrapper)模式。装饰模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案。

  • 抽象构件(Component)角色 :定义一个抽象接口以规范准备接收附加责任的对象。
  • 具体构件(Concrete Component)角色:实现抽象构件,通过装饰角色为其添加一些职责。
  • 抽象装饰(Decorator)角色 :继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
  • 具体装饰(ConcreteDecorator)角色 :实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。

在Go语言中可以通过实现或者匿名组合可以很轻易的实现装饰者模式,装饰者模式又分为全透明和半透明,区别在于是否改变被装饰接口的定义。

// 汽车接口
type Car interface {
   Run()
}

// 卡车
type Truck struct {
}

func (t Truck) Run() {
   fmt.Println("Truck can run")
}

//会飞的卡车
type CarCanFlyDecorator interface {
   Car
}

type TruckDecorator struct {
   car Car
}

// 对原有方法进行增强,没有改变定义
func (t TruckDecorator) Run() {
   t.car.Run()
   fmt.Println("truck can fly")
}

func Test(t *testing.T) {
   var car Car
   car = Truck{}
   car.Run()
   car = TruckDecorator{car}
   car.Run()
}

这是全透明的装饰模式

type Car interface {
   Run()
}

type Truck struct {
}

func (t Truck) Run() {
   fmt.Println("Truck can run")
}

// 修改了接口定义
type CarCanFlyDecorator interface {
   Car
   Fly()
}

type TruckDecorator struct {
   car Car
}

func (t TruckDecorator) Run() {
   t.car.Run()
}

func (t TruckDecorator) Fly() {
   fmt.Println("truck can fly")
}

func Test(t *testing.T) {
   var car Car
   car = Truck{}
   car.Run()
   var flyCar CarCanFlyDecorator
   flyCar = TruckDecorator{car}
   flyCar.Run()
   flyCar.Fly()
}

这是半透明装饰模式,半透明的装饰模式是介于装饰模式和适配器模式之间的。适配器模式的用意是改变所考虑的类的接口,也可以通过改写一个或几个方法,或增加新的方法来增强或改变所考虑的类的功能。大多数的装饰模式实际上是半透明的装饰模式,这样的装饰模式也称做半装饰、半适配器模式。

代理模式和透明装饰者模式的区别

相同点

  • 都要实现与目标类相同的业务接口
  • 都要声明目标对象为成员变量
  • 都可以在不修改目标类的前提下增强目标方法

不同点

  • 目的不同装饰者是为了增强目标对象静态代理是为了保护和隐藏目标对象
  • 获取目标对象构建的地方不同装饰者是由外界传递进来,可以通过构造方法传递静态代理是在代理类内部创建,以此来隐藏目标对象

外观模式

又名门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性,是迪米特法则的典型应用。

  • 外观(Facade)角色:为多个子系统对外提供一个共同的接口。
  • 子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。

例子:以前到家需要自己手动开电视,开空调,现在有了智能控制器,可以直接控制电视和空调,不再需要手动操作

// 电视
type Tv struct {
}

func (t Tv) TurnOn() {
   fmt.Println("电视打开了")
}

func (t Tv) TurnOff() {
   fmt.Println("电视关闭了")
}

// 风扇
type Fan struct {
}

func (f Fan) TurnOn() {
   fmt.Println("风扇打开了")
}

func (f Fan) TurnOff() {
   fmt.Println("风扇关闭了")
}

// 智能遥控器
type Facade struct {
   tv  Tv
   fan Fan
}

func NewFacade() *Facade {
   return &Facade{tv: Tv{}, fan: Fan{}}
}

func (f Facade) TurnOnTv() {
   f.tv.TurnOn()
}

func (f Facade) TurnOnFan() {
   f.fan.TurnOn()
}

func (f Facade) TurnOffTv() {
   f.tv.TurnOff()
}

func (f Facade) TurnOffFan() {
   f.fan.TurnOff()
}

func TestFacade(t *testing.T) {
   facade := NewFacade()
   facade.TurnOnTv()
   facade.TurnOnFan()
   facade.TurnOffTv()
   facade.TurnOffFan()
}

优点

降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。

对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。

缺点

不符合开闭原则,修改很麻烦

桥接模式

将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。

例子:需要开发一个跨平台视频播放器,可以在不同操作系统平台(如Windows、Mac、Linux等)上播放多种格式的视频文件,常见的视频格式包括RMVB、AVI等。该播放器包含了两个维度,适合使用桥接模式。

// 视频文件接口
type VideoFile interface {
   decode() string
}

type AVI struct {
}

func (a AVI) decode() string {
   return "AVI"
}

type RMVB struct {
}

func (R RMVB) decode() string {
   return "RMVB"
}

// 操作系统
type OS struct {
   videoFile VideoFile
}

// 播放文件
func (o OS) Play() {
   panic("overwrite me!")
}

type Windows struct {
   OS
}

func (w Windows) Play() {
   fmt.Println("windows" + w.OS.videoFile.decode())
}

type Mac struct {
   OS
}

func (m Mac) Play() {
   fmt.Println("mac" + m.OS.videoFile.decode())
}

type Linux struct {
   OS
}

func (l Linux) Play() {
   fmt.Println("linux" + l.OS.videoFile.decode())
}

func TestBridge(t *testing.T) {
   os := Windows{OS{videoFile: AVI{}}}
   os.Play()
}

桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。

组合模式

又名部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。

  • 抽象根节点(Component):定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性。
  • 树枝节点(Composite):定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构。
  • 叶子节点(Leaf):叶子节点对象,其下再无分支,是系统层次遍历的最小单位。

抽象根节点定义默认行为和属性,子类根据需求去选择实现和不实现哪些操作,虽然违背了接口隔离原则,但是在一定情况下非常适用。

// 抽象根节点
type Component interface {
   Add(Component)
   Print()
   Remove(int)
}

// 树枝节点
type Composite struct {
   leafs []Component
}

func (c *Composite) Add(component Component) {
   c.leafs = append(c.leafs, component)
}

func (c *Composite) Print() {
   for _, leaf := range c.leafs {
      leaf.Print()
   }
}

func (c *Composite) Remove(index int) {
   c.leafs = append(c.leafs[:index], c.leafs[index+1:]...)
}

func NewComposite() Component {
   return &Composite{make([]Component, 0, 0)}
}

// 叶子节点
type Leaf struct {
   name string
}

// 不支持的操作,无意义调用
func (l Leaf) Add(component Component) {
   panic("Unsupported")
}

func (l Leaf) Print() {
   fmt.Println(l.name)
}

func (l Leaf) Remove(i int) {
   panic("Unsupported")
}

func TestComposite(t *testing.T) {
   composite := NewComposite()
   composite.Add(Leaf{"A"})
   composite.Add(Leaf{"B"})
   composite.Add(Leaf{"C"})
   composite.Add(Leaf{"D"})
   newComposite := NewComposite()
   newComposite.Add(Leaf{name: "E"})
   composite.Add(newComposite)
   composite.Print()
}

组合模式分为透明组合模式和安全组合模式,区别在于,树枝节点与叶子节点在接口的表现上是否一致,前者是完全一致,但是需要额外的处理避免无意义的调用,而后者虽然避免了无意义的调用,但是对于客户端来说不够透明,叶子节点与树枝节点具有不同的方法,以至于不能很好的抽象。

享元模式

运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似对象的开销,从而提高系统资源的利用率。

享元(Flyweight )模式中存在以下两种状态:

1.内部状态,即不会随着环境的改变而改变的可共享部分。

2.外部状态,指随环境改变而改变的不可以共享的部分。享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。

  • 抽象享元角色(Flyweight):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
  • 具体享元(Concrete Flyweight)角色 :它实现了抽象享元类,称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
  • 非享元(Unsharable Flyweight)角色 :并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
  • 享元工厂(Flyweight Factory)角色 :负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。

众所周知的俄罗斯方块中的一个个方块,如果在俄罗斯方块这个游戏中,每个不同的方块都是一个实例对象,这些对象就要占用很多的内存空间,下面利用享元模式进行实现。

// 抽象盒子
type AbstractBox interface {
   Shape() string
   Display(string)
}

// 具体盒子
type Box struct {
   shape string
}

func (b Box) Shape() string {
   return b.shape
}

func (b Box) Display(color string) {
   fmt.Println(b.shape + " " + color)
}

// 盒子工厂
type BoxFactory struct {
   maps map[string]AbstractBox
}

func NewBoxFactory() *BoxFactory {
   return &BoxFactory{maps: map[string]AbstractBox{"I": Box{"I"}, "L": Box{"L"}, "O": Box{"O"}}}
}

func (f BoxFactory) Get(name string) AbstractBox {
   _, ok := f.maps[name]
   if !ok {
      f.maps[name] = Box{name}
   }
   return f.maps[name]
}

func TestBox(t *testing.T) {
   boxFactory := NewBoxFactory()
   boxFactory.Get("I").Display("red")
}

在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。

优点

极大减少内存中相似或相同对象数量,节约系统资源,提供系统性能,享元模式中的外部状态相对独立,且不影响内部状态

缺点

为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂

上次编辑于:
贡献者: 246859