设计模式概述
设计模式是一套被反复使用的、多数人知晓的、经过分类编的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理地运用设计模式可以完美地解决很多问题,每种模式在现实中都有相应的原理来与之对应,每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是设计模式能被广泛应用的原因。
设计模式分类
创建型模式、结构性模式、行为型模式
模式分类
创建型模式
对象实例化模式,创建型模式用于解耦对象实例化过程
结构型模式
把类和对象结合在一起形成一个更大的结构
行为型模式
类和对象如何交互,以及划分责任和算法
23种设置模式分类
设计模式的原则
参考文章中有一句非常精简概括的对六大原则的总结:
单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。
开闭原则
对扩展开放,对修改关闭。在需要修改程序的时候不需要对原有代码进行修改,实现热插拔效果。
在我们进行软件开发的时候经常会有需求要求我们对老的代码进行修改,如果我们直接修改老代码的逻辑则很有可能在代码中引入错误。所以我们要尽量的满足开闭原则,在最代码进行修改的时候较少的更改原有的代码逻辑,将新功能以较少侵入的原则增加进去。
当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
举个形象的例子:
你的刮胡刀的刀片往往都是能随意替换的,一个刀片坏了我们将其卸下换一个新的刮胡刀就能继续使用。但是如果某个家伙将刀片焊死在刮胡刀上,那么这明显不是一个好的设计,因为刀片坏了的时候我们无法直接更好刀片,八成就要各种工具把老刀片卸下来焊上新的。很有可能拆卸过程中刮胡刀就被你搞坏了。
里氏代换原则
里氏代换原则主要是相对于继承来说的,有以下原则
类B
继承于类A
,我们在程序中定义了A a
和B b
两个对象,对于对象a
当我们把a
替换为b
的时候程序的行为不应该受到影响。也就是说所有使用基类的地方都可以使用其子类替换
实际上这个原则就是要限制我们约束子类对父类的方法和参数的的修改,当然不是要禁止修改。
- 子类可以实现父类的抽象方法,但是尽量不要覆盖非抽象方法
- 子类可以增加自己特有的方法
举个形象的例子:
有大头儿子和小头爸爸父子俩,小头爸爸有打酱油的技能,还有个获取妻子名字(围裙妈妈)的方法。大头儿子继承于小头爸爸,那么我们就应该将小头爸爸打酱油的方法声明为普通的方法,大头儿子不需要重写打酱油的方法就可以成功去打酱油。但是获取妻子名字这个方法应该是抽象的,毕竟大头儿子长大了是要有自己的妻子的。
这里如果大头儿子重写了小头爸爸的打酱油的方法,比如小头爸爸打酱油的时候会固定乘汽车去A
店铺打酱油。大头儿子复写了小头爸爸打酱油的方法时,改成了滑滑板去B
店铺打酱油。当我们在调用小头爸爸打酱油的地方替换为了大头儿子打酱油,整个行为预期就被改变了。
依赖倒转原则
高层模块不应该依赖底层模块,二者应该依赖抽象;抽象不应该依赖细节,细节应该依赖于抽象
比如参考文章中讲故事的案例,我们可以拿来简化说一下:
一个母亲给儿子读书上的故事,有母亲类和书的类。母亲类有一个读书的方法,调用方法给儿子讲故事。这样看起来一切都没问题。
突然有一天孩子想让母亲讲报纸上的故事,这就为难了,因为母亲只会读书本上的故事。为了解决这个问题,我们声明了一个读物的接口,接口有一个阅读的方法。让报纸和书同时实现读物的接口,母亲类阅读的方法形参是一个读物的接口,这样当程序想让母亲读书的时候就传入书类的实例,读报纸的时候就传入报纸的实例
这个例子中母亲类是完成阅读的主要业务类,要尽可能不修改目前类
接口隔离原则
客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上通俗解释一下:
我们编写框架的时候应该遵循最小接口的原则,例如我们声明了接口A
里面有方法a1、a2、a3
然后我们声明两个类AImpl1、AImpl2
,其中AImpl1
中使用到了a1、a2
两个方法完成业务,AImpl2
中使用a2、a3
两个方法完成业务。
但是因为我们AImpl1、AImpl2
都继承了A
接口,所以他们都需要实现a1、a2、a3
三个方法,这导致实现了和自己业务完全无关的接口,同时也会降低代码的可阅读性。
改进版
改进版中我们将A1接口拆分成三个接口A1、A2、A3
,这三个接口分别有三个方法a1、a2、a3
。
然后我们的两个业务类中,AImpl1
继承A1、A2
类重写a1、a2
两个方法满足逻辑,然后AImpl2
继承A2、A3
类重写a2、a3
两个方法满足逻辑
这样我们轻松的将逻辑进行了解耦,避免了AImpl1
和Aimpl2
重写与自己无关的接口。
需要注意的是,接口太过细碎也不见得就是好的设计,这个度需要开发者自己把握
迪米特法则
一个对象应该对其它对象保持最少的了解
类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。
程序设计的时候我们一般会遵循低耦合高内聚的原则,迪米特法则就是用于指导我们实现低耦合的。
单一职责
一个类只负责一项职责
仍然用参考文章中的例子说明:
有这样一个程序,创建一个动物类Animinal,动物有呼吸空气的方法。这是一个非常简单职责非常单一的类,完全没用任何问题。
有一天我们突然发现并不是所有动物都是呼吸空气的,比如水里的鱼。这个时候我们是有多种处理方法的。
- 将Animinal拆分为陆生和水生,分别实现呼吸空气和呼吸水两个方法。这样最符合单一职责的原则
- 在Animinal的方法中新增一个呼吸水的方法,这样陆地生物调用呼吸空气方法,水生生物调用呼吸水的方法(这种并不优雅,需要调用者区分生物是陆生还是水生)
- 在原有的呼吸方法中加判读,如果是陆地生物就呼吸空气,如果是水生生物就呼吸水。(比2好一点)
以上三个方式中,方式1是最符合单一职责的,对于这样一个简单的程序方式1的设计最为优美。
对于这样一个简单的类3种方法都不会有太大的复杂度,那么如果现在有了一个新需求,要求将水生动物分为海水动物和淡水动物、陆生生物分为素食和肉食从而加一个吃饭的方法、陆生生物分为胎生和卵生从而加一个繁殖后代的方法。这样程序的复杂度就会提升更多,我们将需求变更带入上面三种实现方式中就会发现单一职责(第一种)的方式是最优解
过于追求单一职责可能会让程序类过于碎片化,仍然是一个度的问题