跳过正文

设计模式之结构型模式

·647 字·4 分钟
Chuck Chan
作者
Chuck Chan
分享技术、思考与生活

结构型模式
#

1.代理模式
#

代理模式可以为其他对象提供一种代理以控制对这个对象的访问。所谓代理,是指具有与代理元(被代理的对象)具有相同的接口的类,客户端必须通过代理与被代理的目标类交互,而代理一般在交互的过程中(交互前后),进行某些特别的处理。代理模式的角色与职责如下:

  • Subject(抽象主题角色):真实主题与代理主题的共同接口。
  • RealSubject(真实主题角色):定义了代理角色所代表的真实对象。
  • Proxy(代理主题角色):含有对真实主题角色的引用,代理角色通常在将客户端调用传递给真实主题对象之前或者之后执行某些操作,而不是单纯返回真实的对象。
package main

import "fmt"

type Goods struct {
	Kind string //商品种类
	Fact bool //商品真伪
}

// =========== 抽象层 ===========
//抽象的购物主题Subject
type Shopping interface {
	Buy(goods *Goods) //某任务
}

// =========== 实现层 ===========
type KoreaShopping struct {}
func (ks *KoreaShopping) Buy(goods *Goods) {
	fmt.Println("去韩国进行了购物, 买了 ", goods.Kind)
}

type AmericanShopping struct {}
func (as *AmericanShopping) Buy(goods *Goods) {
	fmt.Println("去美国进行了购物, 买了 ", goods.Kind)
}

type AfricaShopping struct {}
func (as *AfricaShopping) Buy(goods *Goods) {
	fmt.Println("去非洲进行了购物, 买了 ", goods.Kind)
}

//代理层
type OverseasProxy struct {
	shopping Shopping //代理某个主题,这里是抽象类型
}

func NewProxy(shopping Shopping) *OverseasProxy {
	return &OverseasProxy{
		shopping: shopping,
	}
}

func (op *OverseasProxy) Buy(goods *Goods)  {
	if op.distinguish(goods) { //代理验货
		op.shopping.Buy(goods) //进行购买 调用真实主题角色的具体任务
		op.check(goods) //海关安检
	}
}

//代理验货
func (op *OverseasProxy) distinguish(goods *Goods) bool {
	fmt.Println("对[", goods.Kind,"]进行了辨别真伪.")
	if goods.Fact == false {
		fmt.Println("发现假货",goods.Kind,", 不应该购买。")
	}
	return goods.Fact
}

//安检流程
func (op *OverseasProxy) check(goods *Goods) {
	fmt.Println("对[",goods.Kind,"] 进行了海关检查, 成功的带回祖国")
}

func main()  {
	g1 := &Goods{Kind: "韩国面膜", Fact: false}
	g2 := &Goods{Kind: "米国鞋子", Fact: true}

	//不使用代理模式
	koreaShopping := &KoreaShopping{}
	if g1.Fact == true {
		fmt.Println("对[", g1.Kind,"]进行了辨别真伪.")
		koreaShopping.Buy(&g1)
		fmt.Println("对[",g1.Kind,"] 进行了海关检查, 成功的带回祖国")
	}


	//使用代理模式
	proxy1 := NewProxy(&KoreaShopping{})
	proxy1.Buy(g1)

	proxy2 := NewProxy(&AmericanShopping{})
	proxy2.Buy(g2)
}

优点:

  1. 能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。
  2. 客户端可以针对抽象主题角色进行编程,增加和更换代理类无须修改源代码,符合开闭原则,系统具有较好的灵活性和可扩展性。

缺点:

  1. 代理实现较为复杂。

适用场景:

  1. 为其他对象提供一种代理以控制对这个对象的访问。

2.装饰模式
#

装饰模式动态地给一个对象增加一些额外的职责,就新增对象来说,装饰模式比生成子类实现更加灵活。装饰器模式关注于在一个对象上动态的添加方法,然而代理模式关注于控制对对象的访问。换句话说,用代理模式,代理类(proxy class)可以对它的客户隐藏一个对象的具体信息。因此,当使用代理模式的时候,我们常常在一个代理类中创建一个对象的实例。并且,当我们使用装饰器模式的时候,我们通常的做法是将原始对象作为一个参数传给装饰者的构造器。装饰器模式中的角色与职责如下:

  • Component(抽象构件):它是具体构件与抽象装饰类的共同父类,声明了在具体构件中实现的业务方法,它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作。
  • ConcreteComponent(具体构件):它是抽象构件类的子类,用于定义具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责(方法)。
package main

import (
	"fmt"
)

// ---------- 抽象层 ----------
//抽象构件
type Phone interface {
	Show()
}

//装饰器基础类(该类本应该为interface,但是Golang interface语法不可以有成员属性)
type Decorator struct {
	phone Phone
}


// ----------- 实现层 -----------
// 具体的构件
type HuaWei struct {}
func (hw *HuaWei) Show() {
	fmt.Println("秀出了HuaWei手机")
}

type XiaoMi struct{}
func (xm *XiaoMi) Show() {
	fmt.Println("秀出了XiaoMi手机")
}


//具体的装饰器类
type FilmDecorator struct {
	Decorator   //继承基础装饰器类(主要继承Phone成员属性)
}

func NewFilmDecorator(phone Phone) *FilmDecorator {
	return &FilmDecorator{Decorator{phone: phone}}
}

func (fd *FilmDecorator) Show() {
	fd.phone.Show() //调用被装饰构件的原方法
	fmt.Println("贴膜的手机") //装饰额外的方法
}

type ShellDecorator struct {
	Decorator   //继承基础装饰器类(主要继承Phone成员属性)
}

func NewShellDecorator(phone Phone) *ShellDecorator {
	return &ShellDecorator{Decorator{phone: phone}}
}

func (sd *ShellDecorator) Show() {
	sd.phone.Show() //调用被装饰构件的原方法
	fmt.Println("有手机壳的手机") //装饰额外的方法
}

func main()  {
	huawei := &HuaWei{}
	huawei.Show()  //原构件的方法

	//使用贴膜装饰器 得到新的功能
	fmt.Println("--------------------")
	huawei_film := NewFilmDecorator(huawei)
	huawei_film.Show()

	//使用手机壳装饰器 得到新功能
	fmt.Println("--------------------")
	huawei_shell := NewShellDecorator(huawei)
	huawei_shell.Show()

	//
	fmt.Println("--------------------")
	huawei_film_shell := NewShellDecorator(huawei_film)
	huawei_film_shell.Show()
}

优点:

  1. 对于扩展一个对象的功能,装饰模式比继承更加灵活性,不会导致类的个数急剧增加。
  2. 可以通过一种动态的方式来扩展一个对象的功能,从而实现不同的行为。
  3. 可以对一个对象进行多次装饰。
  4. 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,原有类库代码无须改变,符合“开闭原则”。

缺点:

  1. 使用装饰模式进行系统设计时将产生很多小对象,大量小对象的产生势必会占用更多的系统资源,影响程序的性能。
  2. 装饰模式提供了一种比继承更加灵活机动的解决方案,但同时也意味着比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为繁琐。

适用场景:

  1. 动态、透明的方式给单个对象添加职责。
  2. 当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。

3.适配器模式
#

适配器模式将类的接口转换成客户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的的那些类可以一起工作。适配器模式的角色与职责如下:

  • Target(目标抽象类):目标抽象类定义客户所需接口,可以是一个抽象类或者接口,也可以是具体的类。
  • Adapter(适配器类):适配器可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配,适配器类是适配器模式的核心,在对象适配器中,它通过继承Target并关联一个Adaptee对象使二者产生联系。
  • Adaptee(适配者类):适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码。
package main

import "fmt"

//适配的目标
type V5 interface {
	UseV5()
}

//
type Phone struct {
	v5 V5
}

func NewPhone(v5 V5) *Phone  {
	return &Phone{v5: v5}
}

func (p *Phone) Charge()  {
	fmt.Println("phone进行充电。。。")
	p.v5.UseV5()
}

//被适配的角色 适配者
type V220 struct {

}

func (*V220) Use220V()  {
	fmt.Println("使用220v的电压")
}

//适配器 adapter
type Adapter struct {
	v220 *V220 //被适配的对象
}

func NewAdapter(v220 *V220) *Adapter {
	return &Adapter{v220: v220}
}

func (a *Adapter) UseV5()  {
	fmt.Println("使用适配器进行充电")
	a.v220.Use220V()
}

func main()  {
	iphone := NewPhone(NewAdapter(&V220{}))
	iphone.Charge()
}

优点:

  1. 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构。
  2. 增加了类的透明性和复用性,将具体的业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用。
  3. 灵活性和扩展性都非常好,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。

缺点:

  1. 适配器中置换适配者类的某些方法比较麻烦。

适用场景:

  1. 系统需要使用一些现有的类,而这些类的接口(如方法名)不符合系统的需要,甚至没有这些类的源代码。
  2. 想创建一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。

4.外观模式
#

根据迪米特法则,如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。Facade模式也叫外观模式,Facade模式为一组具有类似功能的类群,比如类库,子系统等等,提供一个一致的简单的界面。这个一致的简单的界面被称作facade。外观模式中的角色与职责如下:

  • Façade(外观角色):为调用方定义简单的调用接口。
  • SubSystem(子系统角色):功能提供者,提供功能的类群(模块或者子系统)。
package main

import "fmt"

//-------------子系统层-------------
//电视机
type TV struct{}

func (t *TV) On() {
	fmt.Println("打开 电视机")
}

func (t *TV) Off() {
	fmt.Println("关闭 电视机")
}

//电视机
type VoiceBox struct{}

func (v *VoiceBox) On() {
	fmt.Println("打开 音箱")
}
func (v *VoiceBox) Off() {
	fmt.Println("关闭 音箱")
}

//灯光
type Light struct{}

func (l *Light) On() {
	fmt.Println("打开 灯光")
}

func (l *Light) Off() {
	fmt.Println("关闭 灯光")
}

//游戏机
type Xbox struct{}

func (x *Xbox) On() {
	fmt.Println("打开 游戏机")
}

func (x *Xbox) Off() {
	fmt.Println("关闭 游戏机")
}

//麦克风
type MicroPhone struct{}

func (m *MicroPhone) On() {
	fmt.Println("打开 麦克风")
}

func (m *MicroPhone) Off() {
	fmt.Println("关闭 麦克风")
}

//投影仪
type Projector struct{}

func (p *Projector) On() {
	fmt.Println("打开 投影仪")
}

func (p *Projector) Off() {
	fmt.Println("关闭 投影仪")
}

//-----------Faced层-------------
//家庭影院
type HomePlayerFaced struct {
	tv    TV
	vb    VoiceBox
	light Light
	xbox  Xbox
	mp    MicroPhone
	pro   Projector
}

//ktv模式
func (hp *HomePlayerFaced) DoKTV()  {
	fmt.Println("家庭影院进入KTV模式")
	hp.tv.On()
	hp.pro.On()
	hp.mp.On()
	hp.light.Off()
	hp.vb.On()
}

//游戏模式
func (hp *HomePlayerFaced) DoGame()  {
	fmt.Println("家庭影院进入Game模式")
	hp.tv.On()
	hp.light.On()
	hp.xbox.On()
}

func main()  {
	homePlayer := new(HomePlayerFaced)
	homePlayer.DoKTV()
	fmt.Println("---------------")
	homePlayer.DoGame()
}

优点:

  1. 它对客户端屏蔽了子系统组件,减少了客户端所需处理的对象数目,并使得子系统使用起来更加容易。通过引入外观模式,客户端代码将变得很简单,与之关联的对象也很少。
  2. 它实现了子系统与客户端之间的松耦合关系,这使得子系统的变化不会影响到调用它的客户端,只需要调整外观类即可。
  3. 一个子系统的修改对其他子系统没有任何影响。

缺点:

  1. 不能很好地限制客户端直接使用子系统类,如果对客户端访问子系统类做太多的限制则减少了可变性和灵活性。
  2. 如果设计不当,增加新的子系统可能需要修改外观类的源代码,违背了开闭原则。

适用场景:

  1. 复杂系统需要简单入口使用。
  2. 客户端程序与多个子系统之间存在很大的依赖性。
  3. 在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度。