天天看点

一、设计模式分类及设计原则

一、设计模式概念

    设计模式(Design pattern)是一套被反复使用、多数人知晓、经过分类编目的、代码设计经验的总结。使用设计模式是为了代码可重用,让代码更容易被他人理解、保证代码的可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的。设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理的运用设计模式可以完美的解决很多问题,每种设计模式在现实生活中都有相应的例子与之相对应,每一个设计模式描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是它被广泛应用的原因。

    在本系列博客开头的标题就提到了“可复用面向对象”这个概念,其实可复用面向对象软件系统一般分为两大类:应用程序工具箱和框架(Framework),我们平时开发的具体软件都是应用程序,java的API属于工具箱;而框架是构成一类特定软件可复用设计的一组相互协作的类。设计模式有助于我们对框架结构的理解,成熟的框架通常使用了很多种设计模式,如果您熟悉这些设计模式,毫无疑问,你将迅速掌握框架的结构。

二、设计模式的分类

    总的来说,设计模式分为三类:

    创建型模式,共五种:

        工厂方法模式(Factory Method)

        抽象工厂模式(Abstract Factory)

        单例模式(Singleton)

        建造者模式(Builder)

        原型模式(Prototype)

    结构型模式,共七种:

        适配器模式(Adapter)

        装饰器模式(Decorator)

        代理模式(Proxy)

        外观模式(Facade)

        桥接模式(Bridge)

        组合模式(Composite)

        享元模式(Flyweight)。

    行为型模式,共十一种:

        策略模式(Strategy)

        模板方法模式(Template method)

        观察者模式(Observer)

        迭代子模式(Iterator)

        责任链模式(Chain of Responsibility)

        命令模式(Command)

        备忘录模式(Memento)

        状态模式(State)

        访问者模式(Visitor)

        中介者模式(Mediator)

        解释器模式(Interpreter)

    其实还有两种:并发模式和线程模式。下面用一个图片来整体描述一下他们的关系:

一、设计模式分类及设计原则

三、设计模式的六大原则(非常重要)

1.开闭原则(Open Closed Principle,OCP)

    此原则是由Bertrand Meyer提出的,原文是:“Software entities should be open for extension, but closed for modification.”。原文也就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,尽量不去修改原有的代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性更好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类。

    那么怎么扩展呢?我们这里以工厂模式(Factory pattern)举例:假设中关村有一个卖盗版盘的小子,我们给他设计一“光盘销售管理软件”。我们应该先设计一“光盘”接口,代码为:

// 光盘接口,接口中有sell方法
type Disc interface {
    sell() //销售方法
}                

而盗版盘是其子类。小子通过“DiscFactory”来管理这些光盘。代码为:

// 光盘工厂
type DiscFactory struct {
    name string
}
// sell implements 
func  (this *DiscFactory) sell() {
    fmt.Println("小伙销售:" + this.name)
}
// getDisc
func (this *DiscFactory) getDisc(name string) Disc {
    return newDisc(name)
}
// new 
func newDisc(_name string) Disc {
    return &DiscFactory{name : _name}
}                

有人要买盗版盘,怎么实现呢?代码为:

discFactory := new(DiscFactory)
var piracy Disc = discFactory.getDisc("盗版光盘")
piracy.sell()                

    如果有一天,这小子良心发现了,开始卖正版软件。没关系,我们只要再创建一个“光盘”的子类“正版软件”就可以了,不需要修改原结构和代码。怎么样?对扩展开放,对修改关闭——“开闭原则”。

    工厂模式是对具体产品进行扩展,有的项目可能需要多的扩展性,要对这个“工厂”也进行扩展,那就成了“抽象工厂模式”。

2.里氏代换原则(Liskov Substitution Principle,LSP)

    里氏代换原则是由Barbara Liskov提出的,LSP是面向对象设计的基本原则之一。它有两种定义:

    第一种定义,最正宗的定义:If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.  它的意思是:如果对每一种类型为T1的对象o1, 都有类型为T2的对象o2, 使得以T1定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。

    第二种定义:functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it. 它的意思是:所有引用基类的地方必须能透明地使用其子类的对象。

    第二个定义是清晰明确的,通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类还不产生任何错误和异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。里氏代换原则是对“开-闭”原则的补充,实现“开-闭”原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。 可以说:里氏代换原则是继承复用的一个基础。

    抄袭网上的一个例子,“正方形不是长方形”是一个理解里氏代换原则最经典的例子。在数学领域里,正方形毫无疑问是一个特殊的长方形,它是一个长宽相等的长方形。所以,我们开发的一个与几何图形相关的软件系统中,让正方形继承长方形是顺理成章的事情。现在,我们截取该系统的一段代码片段分析:

长方形类Rectangle:

package lsp

type Rectangle struct {
	height float32
	width float32
}

func (this * Rectangle) getHeight() float32 {
	return this.height
}

func (this *Rectangle) getWidth() float32 {
	return this.width
}

func (this *Rectangle) setHeight(height float32) {
	this.height = height
}

func (this *Rectangle) setWidth(width float32) {
	this.width = width
}                

正方形类(Square):

package lsp

type Square struct {
	Rectangle
}

func (this * Square) setWidth(width float32) {
	this.Rectangle.setWidth(width)
	this.Rectangle.setHeight(width)
}

func (this *Square) setHeight(height float32) {
	this.Rectangle.setWidth(height)
	this.Rectangle.setHeight(height)
}                

    由于正方形的度和宽度必须相等,所以在方法setHeight和setWidth中,对长度和宽度赋值相同。resize方法是我们的软件系统中的一个组件,然后我们为这个组件编写一个TestResize方法,方法参数需要用到基类Rectangle,TestResize方法的功能是模拟长方形宽度逐步增长的效果:

resize组件代码:

package lsp

import "fmt"

func resize(tangle Rectangle) {
	for tangle.getWidth() <= tangle.getHeight() {
		tangle.setWidth(tangle.getWidth() + 1)
		fmt.Println("长方形宽:", tangle.getWidth())
	}
}                

resize组件测试方法TestResize:

package lsp

import (
	"testing"
)

func TestResize(t * testing.T) {
	tangle := Rectangle{width : 10, height : 10}
	resize(square)
}                

    我们运行一下这段代码就会发现,假如我们把一个普通长方形作为参数传入resize方法,就会看到长方形宽度逐渐增长的效果,当宽度大于长度,代码就会停止,这种行为的结果符合我们的预期;假如我们再把一个正方形作为参数传入resize方法后,就会看到正方形的宽度和长度都在不断增长,代码会一直运行下去,直至系统产生溢出错误。所以,普通的长方形是适合这段代码的,正方形不适合。

    我们得出结论:在resize方法中,Rectangle类型的参数是不能被Square类型的参数所代替,如果进行了替换就得不到预期结果。因此,Square类和Rectangle类之间的继承关系违反了里氏代换原则,它们之间的继承关系不成立,正方形不是长方形。

    抄袭网上的第二个例子。“鸵鸟非鸟”也是一个理解里氏代换原则的经典例子。“鸵鸟非鸟”的另一个版本是”企鹅非鸟“,这两种说法本质上没有区别,前提条件是这种鸟都不会飞。生物学中对于鸟类的定义为:”恒温动物,卵生,全身披有羽毛,身体呈流线形,有角质的喙,眼在头的两侧。前肢退化成翼,后肢有鳞状外皮,有四趾“。所以,从生物学角度来看,鸵鸟肯定是一种鸟。

    我们设计一个与鸟有关的系统,鸵鸟类顺理成章由鸟类派生,鸟类的所有特性和行为都被鸵鸟继承。大多数鸟在人们的印象中都是会飞的,所以,我们给鸟类设计了一个fly方法,还给出了与飞行相关的一些属性,比如飞行速度(velocity)。

鸟类Bird:

type Bird interface {
	fly()
}                

鸵鸟不会飞怎么办?我们就让它扇扇翅膀表示一下吧,在fly方法里什么都不做。至于它的飞行速度,不会飞就只能设定为0了,于是我们就有了鸵鸟类的设计。

鸵鸟类Ostrich:

type Ostrich struct {
}

func (this * Ostrich) fly() {
}

func (this * Ostrich) getVelocity() float64 { 
	return 0
}                

    好了,所有的类都设计完成,我们把类Bird提供给了其它的代码(消费者)使用。现在,消费者使用Bird类完成这样一个需求:计算鸟飞越黄河所需的时间。

    对于Bird类的消费者而言,它只看到了Bird类中有fly和getVelocity两个方法,至于里面的实现细节,它不关心,而且也无需关心,于是给出了实现代码:

func main() {
	ostrich := new(Ostrich)
	var riverWidth float64 = 3000

	var result string
	if (ostrich.getVelocity() != 0) {
		result = strconv.FormatFloat(riverWidth / ostrich.getVelocity(), 'g', 6, 64)
	} else {
		result = "速度为0,不成立"
	}

	fmt.Println(result)
}                

    如果我们拿一种飞鸟来测试这段代码,没有问题,结果正确,符合我们的预期,系统输出了飞鸟飞越黄河的所需要的时间;如果我们再拿鸵鸟来测试这段代码,结果代码发生了系统除零的异常,明显不符合我们的预期。

    对于这段测试代码而言,它只是Bird类的一个消费者,它在使用Bird类的时候,只需要根据Bird类提供的方法进行相应的使用,根本不会关心鸵鸟会不会飞这样的问题,而且也无须知道。它就是要按照“所需时间 = 黄河的宽度 / 鸟的飞行速度”的规则来计算鸟飞越黄河所需要的时间。

    我们得出结论:在测试方法中,Bird类型的参数是不能被Ostrich类型的参数所代替,如果进行了替换就得不到预期结果。因此,Ostrich类和Bird类之间的继承关系违反了里氏代换原则,它们之间的继承关系不成立,鸵鸟不是鸟。

    里氏代换原则讲的是子类与基类的关系,只有当这种关系存在时,里氏代换原则才存在,反之则不存在。里氏替换原则为良好的继承定义了一个规范,一句简单的定义包含了四层含义(四层含义具体例子可参考:里氏替换原则):

  • 子类必须完全实现父类的方法;
  • 子类可以有自己的个性;
  • 覆盖或实现父类的方法时输入参数可以被放大;
  • 覆盖或实现父类的方法时输出结果可以被缩小。

3.依赖倒转原则(Dependence Inversion Principle,DIP)

    依赖倒置的原始定义是:“High level modules should not depend upon low level modules. Both Should depend upon Abstractions. Abstractions should not depend upon details. Details should depend upon abstractions.”。翻译过来包含三层含义:

  • 高层模块不应该依赖于低层模块,两层都应该依赖其抽象;
  • 抽象不应该依赖细节;
  • 细节应该依赖抽象。

    依赖倒置原则是开闭原则的基础,依赖倒置原则的核心思想是面向接口编程,而不是针对实现编程。高层模块和低层模块容易理解,每一个逻辑的实现都是由原子逻辑组成的,不可分割的原子逻辑就是低层模块,原子逻辑的再组装就是高层模块。那什么是抽象,什么又是细节呢?抽象就是指接口或抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或继承抽象类而产生的类就是细节,其特点就是可以直接被实例化,也就是可以加上一个关键字new产生一个对象。

    这里我们用一个例子来说明面向接口编程比相对于面向实现编程好在什么地方。场景是这样的,母亲给孩子讲故事,只要给她一本书,她就可以照着书给孩子讲故事了。代码如下:

package main 

import "fmt"

// declare Book obejct
type Book struct {}
func (this * Book) getContent() string {
	return "很久很久以前,有一个..."
}

// declare Mother object
type Mother struct {}
func (this * Mother) narrate(book Book) {
	fmt.Println("妈妈开始讲故事")
	fmt.Println(book.getContent())
}

// Test
func main() {
	mother := new(Mother)
	mother.narrate(Book{})
}                

运行良好,假如有一天,需求变成这样:不是给书而是给一份报纸,让这位母亲讲一下报纸上的故事,报纸的代码如下:

type Newspaper struct {}
func (this *Newspaper) getContent() string {
	return "林书豪...."
}                

这位母亲却办不到,因为她居然不会读报纸上的故事,这太荒唐了,只是将书换成报纸,居然必须要修改Mother才能读。假如以后需求换成杂志呢?换成网页呢?还要不断地修改Mother,这显然不是好的设计。原因就是Mother与Book之间的耦合性太高了,必须降低他们之间的耦合度才行。

我们引入一个抽象的接口IReader。读物,只要是带字的都属于读物:

type IReader interface {
	getContent() string
}                

other类与接口IReader发生依赖关系,而Book和Newspaper都属于读物的范畴,他们各自都去实现IReader接口,这样就符合依赖倒置原则了,代码修改为:

package main 

import "fmt"

// interface 
type IReader interface {
	getContent() string
}

// declare Book obejct
type Book struct {}
func (this * Book) getContent() string {
	return "很久很久以前,有一个..."
}

type Newspaper struct {}
func (this *Newspaper) getContent() string {
	return "林书豪...."
}

// declare Mother object
type Mother struct {}
func (this * Mother) narrate(reader IReader) {
	fmt.Println("妈妈开始讲故事")
	fmt.Println(reader.getContent())
}

// Test方法
func main() {
	mother := new(Mother)
	mother.narrate(new(Book))
	mother.narrate(new(Newspaper))
}                

    这样修改后,无论以后怎样扩展Test方法,都不需要再修改Mother类了。这只是一个简单的例子,实际情况中,代表高层模块的Mother类将负责完成主要的业务逻辑,一旦需要对它进行修改,引入错误的风险极大。所以遵循依赖倒置原则可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险。

    采用依赖倒置原则给多人并行开发带来了极大的便利,比如上例中,原本Mother类与Book类直接耦合时,Mother类必须等Book类编码完成后才可以进行编码,因为Mother类依赖于Book类。修改后的程序则可以同时开工,互不影响,因为Mother与Book类一点关系也没有。参与协作开发的人越多、项目越庞大,采用依赖导致原则的意义就越重大。现在很流行的TDD开发模式就是依赖倒置原则最成功的应用。

    以抽象耦合是依赖倒转原则的关键,由于一个抽象耦合关系总是涉及具体类从抽象类继承,并且需要保证在任何引用到基类的地方都可以换成其子类,因此,里氏替换原则是依赖倒转原则的基础。

    在抽象层次上的耦合虽然有灵活性,但也带来了额外的复杂性,在某些情况下,如果一个具体类发生变化的可能性很小,那么抽象耦合能发挥的好处便十分有限,这时使用具体耦合反而更好。

    在实际编程中,我们一般需要做到如下3点:

  • 低层模块尽量都要有抽象类或接口,或者两者都有。
  • 变量的声明类型尽量是抽象类或接口。
  • 使用继承时遵循里氏替换原则。

4.接口隔离原则(Interface Segregation Principle,ISP)

    一个接口代表一个角色,不应当将不同的角色都交给一个接口。没有关系的接口合并在一起,形成一个臃肿的打接口,这是对角色和接口的污染,使用多个专门的接口比使用单一的接口要好,一个类对另外一个类的依赖性应当是建立在最小的接口上的。

    用一句通俗的话说就是,不要强迫客户使用它们不用的方法,如果强迫用户使用它们不使用的方法,那么这些客户就会面临由于这些不使用的方法的改变所带来的改变。

    用一个“电子商务的系统”的例子说明ISP原则。系统中存在订单这个类,有三个地方需要用到:

  • 一个是门户,只能有查询方法,
  • 一个是外部系统,有添加订单的方法,
  • 一个是管理后台,添加删除修改查询都要用到。

    根据接口隔离原则(ISP),一个类对另外一个类的依赖应当是建立在最小的接口上。也就是说,对于门户,它只能依赖有一个查询方法的接口。

    UML结构如下:

一、设计模式分类及设计原则

下面是实现的代码:

// interface 
type IOrderForPortal interface {
    getOrder() string
}
type IOrderForOtherSys interface {
    insertOrder() string
}
type IOrderForAdmin interface {
    IOrderForPortal
    IOrderForOtherSys
    updateOrder() string
    deleteOrder() string
}
//implements
type Order struct{}
func (this *Order) getOrder() string {
    return "--getOrder--"
}
func (this *Order) insertOrder() string {
    return "--insertOrder--"
}
func (this *Order) updateOrder() string {
    return "--updateOrder--"
}
func (this *Order) deleteOrder() string {
    return "--deleteOrder--"
}                

5.合成\聚合复用原则(Composite/Aggregate Reuse Principle,CARP)

    合成/聚合复用原则经常又叫做合成复用原则。聚合表示整体与部分的关系,表示“含有”,整体由部分组合而成,部分可以脱离整体作为一个独立的个体存在。组合则是一种更强的聚合,部分组成整体,而且不可分割,部分不能脱离整体而单独存在。

    合成/聚合复用原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新对象通过向这些对象的委派达到复用已有功能的目的。它的设计原则是:要尽量使用合成/聚合,尽量不要使用继承。

    就是说要少用继承,多用合成关系来实现。比如:有几个类要与数据库打交道,我们就写了一个操作数据库的类,然后别的跟数据库打交道的类都继承这个,后来需要修改数据库操作类的一个方法,各个类都需要修改。“牵一发而动全身”!面向对象是要把波动限制在尽量小的范围。

6.最小知识原则(Principle of Least Knowledge,PLK,也叫迪米特法则)

    也叫迪米特法则。不要和陌生人说话,即一个对象应对其他对象有尽可能少的了解。个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。

四、总结

     开闭原则具有理想主义的色彩,它是面向对象设计的终极目标。其他几条,则可以看做是开闭原则的实现方法。设计模式就是实现了这些原则,从而达到了代码复用、增加可维护性的目的。 理解了这些原则,再看 设计 模式,只是在具体问题上怎么实现这些原则而已。 张无忌 学 太极 ,忘记了所有招式,打倒了“ 玄冥二老 ”,所谓“心中无招”。设计模式可谓招数,如果先学通了各种模式,又忘掉了所有模式而随心所欲,可谓OO(Object-Oriented,面向对象)之最高境界。呵呵,搞笑,搞笑!

    注: 如有疑问和建议,请广大朋友不吝指正。谢谢!!!

版权声明:本文为CSDN博主「weixin_33705053」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/weixin_33705053/article/details/92037632