设计模式入门
简介
设计模式,是对前人遇到问题并顺利解决问题方案的一种总结,是一种经验复用。
UML关系图
1)泛化,可以简单地理解为继承关系;
2)实现,一般是接口和实现类之间的关系;
3)关联,一种拥有关系,比如老师类中有学生列表,那么老师类和学生类就是拥有关系;
4)聚合,整体与部分的关系,但是整体和部分是可以分离而独立存在的,如汽车类和轮胎类;
5)组合,整体与部分的关系,但是二者不可分离,分离了就没有意义了,例如,公司类和部门类,没有公司就没有部门;
6)依赖,一种使用关系,例如创建 A 类必须要有 B 类。
案例
下面我们从一个游戏说起
需求:做一个模拟鸭子的游戏,游戏中有各种鸭子,一边游泳,一边呱呱叫。
实现:设计一个鸭子超类,让各种鸭子继承这个超类。
实现说明:
1)由于所有的鸭子都会呱呱叫和游泳,所以这部分代码由超类来具体实现
2)由于每种鸭子的外观各不相同,所以超类中的display()方法是抽象的,每个鸭子子类负责自己的display()方法的具体实现
需求变更:需要会飞的鸭子
具体实现:在超类Duck中加入fly()方法,然后让所有的鸭子子类都会继承fly()方法
发现问题:并非所有的Duck子类都会飞,会使得某些不适合该行为的子类也具有该行为
这同时也说明一个问题,在涉及代码维护时,为了复用代码的目的而使用继承,有时结局并不完美。
分析
1)对代码的局部修改影响层面并不只是局部,在超类中加入fly()会导致所有的子类都具有fly()
2)如果在某些不需要fly()方法的子类中覆盖掉此方法,会怎么样
V
如果我加入一个橡皮鸭,只会吱吱叫,不会飞,需要覆盖超类quack()的方法,改为吱吱叫,覆盖超类fly()的方法,改为什么都不做
如果我再加入一个木头鸭子,不会飞也不会叫,需要同时覆盖fly()和quack(),改为什么都不做
每当加入一个新的鸭子时都要检查是否覆盖fly()和quack(),这简直是无穷无尽的噩梦
V
我们发现利用继承提供Duck的行为,会导致代码在多个子类中重复
运行时的行为不容易改变,很难知道鸭子的全部行为
改变会牵一发而动全身,造成其他鸭子不想要的改变
添加需求:每六个月更新一次产品
具体实现:把fly()从超类中提取出来放进一个Flyable接口中,只有实现此接口的鸭子类型才会飞,quack()亦如此
需要让某些鸭子子类可以飞或可以叫,而不是全部
分析
Flyable和Quackable能够解决一部分问题,但是却无法造成代码复用,
在会飞的鸭子子类中,鸭子的飞行动作可能有多种变化,
比如,50个子类实现Flyable接口后都需要稍微修改一下飞行的行为
V
我们希望有一种方法,让我们用一种对既有代码影响最小的方式来修改软件
小结
继承并不能很好的解决问题,因为鸭子的行为在子类中不断的改变,并且让所有的子类都具有这些行为并不恰当,
Flyable和Quackable接口一开始似乎不错,解决了只有会飞的鸭子才继承Flyable接口,但是JAVA接口不具有实现具体代码的功能,
所有继承接口无法实现代码的复用,这意味着无论何时你需要修改某个行为,必须得在每一个定义此行为的类中去修改它。
设计原则一:将变化的部分与不变的部分分离
软件是需要成长和改变的,否则就会dead。
有一个设计原则可以帮助我们解决现在遇到的问题 ,把软件中可能需要变化的部分抽离并封装起来,
以便以后可以轻易的改动或扩充这部分,并不影响其他不需要改变的部分,系统因此变得具有弹性。
变化的部分与不变的部分
那么针对我们上面的Duck超类,需要改变的部分是哪些呢?
分析
Duck类中的fly()和quack()会随着鸭子的不同而改变
V
为了把fly()和quack()这两个行为从Duck中分离出来
我们将建立两组新类来各自代表fly()行为和quack()行为
多种行为的实现分别放在这两组类中
变化的部分:fly()和quack()
不变的部分:Duck类
具体实现:建立两组类,一组fly()相关,一组quack()相关,每一组类实现各自的动作,
比如有一个类实现“呱呱叫”,有一个类实现“吱吱叫”,还有一个类实现“安静”
设计原则二:针对接口编程,而不是针对实现编程
设计鸭子的行为
鸭子的行为没有弹性
我们希望指定行为到鸭子的实例中
V
设计一个新的绿头鸭
指定特定类型的飞行行为给它
顺便让鸭子的行为可以动态改变
V
利用接口代表每个行为,比如FlyBehavior, QuackBehavior
行为的每个实现都将实现其中一个接口
V
Duck类不负责实现Flying和Quacking接口
而是由我们制造一组其他行为类去专门实现FlyBehavior和 QuackBehavior
V
鸭子的行为被放在分开的类中,此类专门提供某行为接口的实现
这样,鸭子就不需要知道行为的具体实现细节
以往做法
行为来自Duck超类的具体实现或是继承某个接口并由子类自己实现,这两种做法都依赖于实现,
使得我们被实现绑得死死的,只能写更多的代码去更改行为。
现在的做法
鸭子的子类将使用接口(FlyBehavior, QuackBehavior)表示行为,实际的实现不会被绑死在鸭子的子类中,
特定的具体行为编写在实现了接口(FlyBehavior, QuackBehavior)的类中。
实现说明
1)所有的飞行类都实现FlyBehavior接口,所有新的分行类都必须实现fly()方法
2)这样的设计可以让fly()和quack()的行为被其他对象复用,因为这些行为已与鸭子类无关
3)我们在新增行为时,不会影响到现有的行为类,也不会影响使用到飞行行为的鸭子类
针对接口编程的理解
针对接口编程的真正意思是针对类型编程。
针对接口编程,而不是针对实现编程。
针对接口变成的关键在于多态,利用多态,程序就可以针对超类编程,执行时会根据实际状况执行到
具体实现类的行为,而不是死死绑定在超类的行为上。
更明确的说,变量的声明应该是一个接口型或抽象类类型,如此只要是具体实现此接口或抽象类的类所产生的实例对象,
都可以指定给这个变量,这意味着声明类时不用理会以后执行时的真正对象类型。
这样做的好处是,子类实例化的动作不需要在代码中硬编码,而是在运行时根据需要指定具体的实现类对象。
我们不知道实际的子类型是什么,我们只关心它知道如何正确的进行调用方法。
设计原则三: 多用组合,少用继承
分析
每一个鸭子都有一个FlyBehavior对象和一个QuackBehavior对象
将飞行和呱呱叫的行为委托给它们处理
V
鸭子的行为不是继承来的,而是通过适当的行为对象组合而来
优点
使用组合建立的系统具有很大的弹性;
不仅可以将算法族封装成类,还可以在运行时动态的改变其行为
参考资料:经典设计模式实战
参考资料:《Head First设计模式》