GO与设计模式(一)

概述 模式名称 模式名称 作用 创建型模式 Creational Pattern (6) 单例模式 ★★★★☆ 是保证一个类仅有一个实例,并提供一个访问它的全局访问点。 简单工厂模式 ★★★☆☆ 通过专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。

概述

模式名称 模式名称 作用
创建型模式 Creational Pattern
(6) 单例模式
★★★★☆ 是保证一个类仅有一个实例,并提供一个访问它的全局访问点。
简单工厂模式
★★★☆☆ 通过专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。
工厂方法模式
★★★★★ 定义一个创建产品对象的工厂接口,将实际创建工作推迟到子类中。
抽象工厂模式
★★★★★ 提供一个创建一系列相关或者相互依赖的接口,而无需指定它们具体的类。
原型模式
★★★☆☆ 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
建造者模式
★★☆☆☆ 将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
结构型模式
Structural Pattern
(7) 适配器模式
★★★★☆ 将一个类的接口转换成客户希望的另外一个接口。使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
桥接模式
★★★☆☆ 将抽象部分与实际部分分离,使它们都可以独立的变化。
组合模式
★★☆☆☆ 将对象组合成树形结构以表示“部分–整体”的层次结构。使得用户对单个对象和组合对象的使用具有一致性。
装饰模式
★★★☆☆ 动态的给一个对象添加一些额外的职责。就增加功能来说,此模式比生成子类更为灵活。
外观模式
★★★★★ 为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
享元模式
★☆☆☆☆ 以共享的方式高效的支持大量的细粒度的对象。
代理模式
★★★★☆ 为其他对象提供一种代理以控制对这个对象的访问。
行为型模式
Behavioral Pattern
(11) 职责链模式
★★☆☆☆ 在该模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求,这使得系统可以在不影响客户端的情况下动态地重新组织链和分配责任。
命令模式
★★★★☆ 将一个请求封装为一个对象,从而使你可用不同的请求对客户端进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
解释器模式
★☆☆☆☆ 如何为简单的语言定义一个语法,如何在该语言中表示一个句子,以及如何解释这些句子。
迭代器模式
★☆☆☆☆ 提供了一种方法顺序来访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
中介者模式
★★☆☆☆ 定义一个中介对象来封装系列对象之间的交互。终结者使各个对象不需要显示的相互调用 ,从而使其耦合性松散,而且可以独立的改变他们之间的交互。
备忘录模式
★★☆☆☆ 是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
观察者模式
★★★★★ 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
状态模式
★★☆☆☆ 对象的行为,依赖于它所处的状态。
                                                                          |

| | 策略模式
★★★★☆ | 准备一组算法,并将每一个算法封装起来,使得它们可以互换。 |
| | 模板方法模式
★★★☆☆ | 使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。 |
| | 访问者模式
★☆☆☆☆ | 表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。 |

面向对象设计原则

名称 定义
单一职责原则
(Single Responsibility Principle, SRP)
★★★★☆ 类的职责单一,对外只提供一种功能,而引起类变化的原因都应该只有一个。
开闭原则
(Open-Closed Principle, OCP)
★★★★★ 类的改动是通过增加代码进行的,而不是修改源代码。
里氏代换原则
(Liskov Substitution Principle, LSP
★★★★★ 任何抽象类(interface接口)出现的地方都可以用他的实现类进行替换,实际就是虚拟机制,语言级别实现面向对象功能。
依赖倒转原则
(Dependence Inversion Principle, DIP)
★★★★★ 依赖于抽象(接口),不要依赖具体的实现(类),也就是针对接口编程。
接口隔离原则
(Interface Segregation Principle, ISP
★★☆☆☆ 不应该强迫用户的程序依赖他们不需要的接口方法。一个接口应该只提供一种对外功能,不应该把所有操作都封装到一个接口中去。
合成复用原则
(Composite Reuse Principle, CRP)
★★★★☆ 如果使用继承,会导致父类的任何变换都可能影响到子类的行为。如果使用对象组合,就降低了这种依赖关系。对于继承和组合,优先使用组合。
迪米特法则
(Law of Demeter, LoD
★★★☆☆ 一个对象应当对其他对象尽可能少的了解,从而降低各个对象之间的耦合,提高系统的可维护性。例如在一个程序中,各个模块之间相互调用时,通常会提供一个统一的接口来实现。这样其他模块不需要了解另外一个模块的内部实现细节,这样当一个模块内部的实现发生改变时,不会影响其他模块的使用。(黑盒原理)

1 单一职责原则

类的职责单一,对外只提供一种功能,引起类变化的原因都应该只有一个

package main

import "fmt"

type ClothesShop struct {}

func (cs *ClothesShop) OnShop() {
	fmt.Println("休闲的装扮")
}

type ClothesWork struct {}

func (cw *ClothesWork) OnWork() {
	fmt.Println("工作的装扮")
}

func main() {
	//工作的时候
	cw := new(ClothesWork)
	cw.OnWork()

	//shopping的时候
	cs := new(ClothesShop)
	cs.OnShop()
}

面向对象编程的过程中,设计一个类,建议对外提供的功能单一,接口单一,影响一个类的范围就只限定在这一个接口上,一个类的一个接口具备这个类的功能含义,职责单一不复杂。

2 开闭原则

类的改动是通过增加代码进行的,而不是修改源代码。

那么作为interface数据类型,他存在的意义在哪呢? 实际上是为了满足一些面向对象的编程思想。我们知道,软件设计的最高目标就是高内聚,低耦合。那么其中有一个设计原则叫开闭原则。什么是开闭原则呢,接下来我们看一个例子:

package main

import "fmt"

//我们要写一个类,Banker银行业务员
type Banker struct {
}

//存款业务
func (this *Banker) Save() {
	fmt.Println( "进行了 存款业务...")
}

//转账业务
func (this *Banker) Transfer() {
	fmt.Println( "进行了 转账业务...")
}

//支付业务
func (this *Banker) Pay() {
	fmt.Println( "进行了 支付业务...")
}

func main() {
	banker := &Banker{}

	banker.Save()
	banker.Transfer()
	banker.Pay()
}

代码很简单,就是一个银行业务员,他可能拥有很多的业务,比如Save()存款、Transfer()转账、Pay()支付等。那么如果这个业务员模块只有这几个方法还好,但是随着我们的程序写的越来越复杂,银行业务员可能就要增加方法,会导致业务员模块越来越臃肿。

Untitled.png

这样的设计会导致,当我们去给Banker添加新的业务的时候,会直接修改原有的Banker代码,那么Banker模块的功能会越来越多,出现问题的几率也就越来越大,假如此时Banker已经有99个业务了,现在我们要添加第100个业务,可能由于一次的不小心,导致之前99个业务也一起崩溃,因为所有的业务都在一个Banker类里,他们的耦合度太高,Banker的职责也不够单一,代码的维护成本随着业务的复杂正比成倍增大。

开闭原则设计

那么,如果我们拥有接口, interface这个东西,那么我们就可以抽象一层出来,制作一个抽象的Banker模块,然后提供一个抽象的方法。 分别根据这个抽象模块,去实现支付Banker(实现支付方法),转账Banker(实现转账方法)

Untitled.png

那么依然可以搞定程序的需求。 然后,当我们想要给Banker添加额外功能的时候,之前我们是直接修改Banker的内容,现在我们可以单独定义一个股票Banker(实现股票方法),到这个系统中。 而且股票Banker的实现成功或者失败都不会影响之前的稳定系统,他很单一,而且独立。

所以以上,当我们给一个系统添加一个功能的时候,不是通过修改代码,而是通过增添代码来完成,那么就是开闭原则的核心思想了。所以要想满足上面的要求,是一定需要interface来提供一层抽象的接口的。

package main

import "fmt"

//抽象的银行业务员
type AbstractBanker interface{
	DoBusi()	//抽象的处理业务接口
}

//存款的业务员
type SaveBanker struct {
	//AbstractBanker
}

func (sb *SaveBanker) DoBusi() {
	fmt.Println("进行了存款")
}

//转账的业务员
type TransferBanker struct {
	//AbstractBanker
}

func (tb *TransferBanker) DoBusi() {
	fmt.Println("进行了转账")
}

//支付的业务员
type PayBanker struct {
	//AbstractBanker
}

func (pb *PayBanker) DoBusi() {
	fmt.Println("进行了支付")
}

// 抽象架构层
func BankBusiness(banker AbstractBanker){
	// 通过接口向下调用(多态)
	// 为不同数据类型的实体提供统一的接口
	banker.DoBusi()
}

func main() {
	//进行存款
	sb := &SaveBanker{}
	sb.DoBusi()

	//进行转账
	tb := &TransferBanker{}
	tb.DoBusi()
	
	//进行支付
	pb := &PayBanker{}
	pb.DoBusi()
	
	// 如果用抽象的架构
	// 对于不同的AbstructBanker 都可以用BankBusiness方法去调用到
	// 对象实际的方法
	BankBusiness(sb)
	BankBusiness(tb)
	BankBusiness(pb)

}

3 里氏代换原则

任何抽象类(interface接口)出现的地方都可以用他的实现类进行替换,实际就是虚拟机制,语言级别实现面向对象功能。

鼓励我们用接口编程,而不是用实例

4 依赖倒转原则

依赖于抽象(接口),不要依赖具体的实现(类),也就是针对接口编程。

假如不用依赖倒转:

package main

import "fmt"

// === > 奔驰汽车 <===
type Benz struct {

}

func (this *Benz) Run() {
	fmt.Println("Benz is running...")
}

// === > 宝马汽车  <===
type BMW struct {

}

func (this *BMW) Run() {
	fmt.Println("BMW is running ...")
}


//===> 司机张三  <===
type Zhang3 struct {
	//...
}

func (zhang3 *Zhang3) DriveBenZ(benz *Benz) {
	fmt.Println("zhang3 Drive Benz")
	benz.Run()
}

func (zhang3 *Zhang3) DriveBMW(bmw *BMW) {
	fmt.Println("zhang3 drive BMW")
	bmw.Run()
}

//===> 司机李四 <===
type Li4 struct {
	//...
}

func (li4 *Li4) DriveBenZ(benz *Benz) {
	fmt.Println("li4 Drive Benz")
	benz.Run()
}

func (li4 *Li4) DriveBMW(bmw *BMW) {
	fmt.Println("li4 drive BMW")
	bmw.Run()
}

func main() {
	//业务1 张3开奔驰
	benz := &Benz{}
	zhang3 := &Zhang3{}
	zhang3.DriveBenZ(benz)

	//业务2 李四开宝马
	bmw := &BMW{}
	li4 := &Li4{}
	li4.DriveBMW(bmw)
}

我们来看上面的代码和图中每个模块之间的依赖关系,实际上并没有用到任何的interface接口层的代码,显然最后我们的两个业务 张三开奔驰, 李四开宝马,程序中也都实现了。但是这种设计的问题就在于,小规模没什么问题,但是一旦程序需要扩展,比如我现在要增加一个丰田汽车 或者 司机王五, 那么模块和模块的依赖关系将成指数级递增,想蜘蛛网一样越来越难维护和捋顺。

Untitled.png

如果我们在设计一个系统的时候,将模块分为3个层次,抽象层、实现层、业务逻辑层。那么,我们首先将抽象层的模块和接口定义出来,这里就需要了interface接口的设计,然后我们依照抽象层,依次实现每个实现层的模块,在我们写实现层代码的时候,实际上我们只需要参考对应的抽象层实现就好了,实现每个模块,也和其他的实现的模块没有关系,这样也符合了上面介绍的开闭原则。这样实现起来每个模块只依赖对象的接口,而和其他模块没关系,依赖关系单一。系统容易扩展和维护。

我们在指定业务逻辑也是一样,只需要参考抽象层的接口来业务就好了,抽象层暴露出来的接口就是我们业务层可以使用的方法,然后可以通过多态的线下,接口指针指向哪个实现模块,调用了就是具体的实现方法,这样我们业务逻辑层也是依赖抽象成编程。

我们就将这种的设计原则叫做依赖倒转原则

修改后的代码:

package main

import "fmt"

// ===== >   抽象层  < ========
type Car interface {
	Run()
}

type Driver interface {
	Drive(car Car)
}

// ===== >   实现层  < ========
type BenZ struct {
	//...
}

func (benz * BenZ) Run() {
	fmt.Println("Benz is running...")
}

type Bmw struct {
	//...
}

func (bmw * Bmw) Run() {
	fmt.Println("Bmw is running...")
}

type Zhang_3 struct {
	//...
}

func (zhang3 *Zhang_3) Drive(car Car) {
	fmt.Println("Zhang3 drive car")
	car.Run()
}

type Li_4 struct {
	//...
}

func (li4 *Li_4) Drive(car Car) {
	fmt.Println("li4 drive car")
	car.Run()
}


// ===== >   业务逻辑层  < ========
func main() {
	//张3 开 宝马
	var bmw Car
	bmw = &Bmw{}

	var zhang3 Driver
	zhang3 = &Zhang_3{}

	zhang3.Drive(bmw)

	//李4 开 奔驰
	var benz Car
	benz = &BenZ{}

	var li4 Driver
	li4 = &Li_4{}

	li4.Drive(benz)
}

5 接口隔离原则

不应该强迫用户的程序依赖他们不需要的接口方法。一个接口应该只提供一种对外功能,不应该把所有操作都封装到一个接口中去。

6 合成复用原则

如果使用继承,会导致父类的任何变换都可能影响到子类的行为。如果使用对象组合,就降低了这种依赖关系。对于继承和组合,优先使用组合。

继承可能继承了过多不需要的方法,使得变的很重量

package main

import "fmt"

type Cat struct {}

func (c *Cat) Eat() {
	fmt.Println("小猫吃饭")
}

//给小猫添加一个 可以睡觉的方法 (使用继承来实现)
type CatB struct {
	Cat  // 匿名cat的话 CatB可以直接调用CatB.Eat() 类似于继承了Cat的方法
}

func (cb *CatB) Sleep() {
	fmt.Println("小猫睡觉")
}

//给小猫添加一个 可以睡觉的方法 (使用组合的方式)
type CatC struct {
	C *Cat  // 组合的话CatC必须通过C成员去调用Eat()
}

func (cc *CatC) Sleep() {
	fmt.Println("小猫睡觉")
}


func main() {
	//通过继承增加的功能,可以正常使用
	cb := new(CatB)
	cb.Eat()
	cb.Sleep()

	//通过组合增加的功能,可以正常使用
	cc := new(CatC)
	cc.C = new(Cat)
	cc.C.Eat()
	cc.Sleep()
}

7 迪米特法则(知道最少原则)

一个对象应当对其他对象尽可能少的了解,从而降低各个对象之间的耦合,提高系统的可维护性。例如在一个程序中,各个模块之间相互调用时,通常会提供一个统一的接口来实现。这样其他模块不需要了解另外一个模块的内部实现细节,这样当一个模块内部的实现发生改变时,不会影响其他模块的使用。(黑盒原理)

原则:高内聚,低耦合

比如go-redis里面 对每个数据 那都可以封装成一个数据实体,我的上层函数只对数据实体做操作,而不关系数据实体里面到底是什么类型的数据(string也好、int也好)

另外比如redis的回复 所有不同的回复都抽象成reply接口,实现通用的一个ToBytes返回byte数组的方法,这样不管你的reply是err还是int的 自己作为一个errreply结构体实现自己的reply方法;而在其它函数调用 比如执行命令,需要返回一个reply 就不需要关系你具体是errreply还是其它,都统一为reply

Comment