仅用于自己学习做笔记使用,路漫漫其修远兮
1.OO设计原则
1.1 问题引入
对应电子书的P38-P45
简述:设计一个模拟鸭子的游戏,游戏中会出现各种鸭子,一边游泳,一边呱呱叫。此系统的内部设计使用了标准的OO技术,设计了一个鸭子的超类,并让各种鸭子继承此超类。
需求更新1:增加新的鸭子类,如会飞的鸭子。
解决方法--利用继承:Joe(书中的菜鸡,也就是本菜鸡本鸡了)提出的想法是在超类中添加一个fly()的抽象类方法:
public abstract class Duck {
abstract public void quack();
abstract public void swim();
abstract public void display();
abstract public void fly();
}
一旦添加之后,则所有的鸭子,不管会不会飞,则都需要重写该方法,如果不会飞的话,则重写该方法,方法实现里什么都不做。
需求更新2:指定鸭子的叫声
解决方法--利用继承:对于会叫和不会叫的鸭子,分别以具体内容重写和空内容重写来实现。
带来的问题:每增加鸭子的新种类,都需要检查并可能需要重写的fly()和quark(),维护性很差
改用接口看似解决了问题,实际还是带来了问题:将fly()从超类中提取出来,放入一个Flyable接口中,只有会飞的鸭子才实现此接口。会叫的同理。这样会相对的解决一部分问题,因为只需要让某些会飞会叫的鸭子类型实现该接口即可。
但是Java接口不具有实现代码,所以继承接口无法达到代码的复用。这意味着:无论何时你需要修改某个行为,必须要往下追踪并在每一个定义次行为的类中修改它。。
怎么解决?
使用良好的OO软件设计原则
软件开发的一个不变真理--CHANGE,改变
因为软件总是需要改变的,可能是因为用户需求、新功能、数据库产品的替换
1.2 设计原则1-找出应用中需要变化的部分,将其“封装”起来,让其他不需要变化的部分不会受影响
前述:使用继承不能很好的解决问题,因为鸭子的行为在子类中不断改变,并且让所有的子类都有这些行为是不恰当的。而接口看似还不错,解决了问题(只有会飞的鸭子才继承Flyable),但是Java接口不具有实现代码,所以继承接口无法达到代码的复用。这意味着:无论何时你需要修改某个行为,必须要往下追踪并在每一个定义次行为的类中修改它,一不小心就会造成新的错误。
设计原则:找出应用中需要变化的部分,将其“封装”起来,让其他不需要变化的部分不会受影响。
把会变化的部分取出并封装起来,以便以后可以轻易地改动或扩充此部分,而不影响不需要变化的其他部分。
这个概念很简单,几乎是每个设计模式背后的精神所在。所有的模式都提供了一套方法让“系统中的某部分改变不影响其他部分”
1.开始动手:分开变化和不变的部分
要分开“变化和不变的部分”,需要建立两组类(完全远离Duck类),一个是"fly"相关的,一个是"quack"先关的,每一组类将实现各自的动作。比如,可以有一个类实现“呱呱叫”,另一个类实现“吱吱叫”,还有一个类实现“安静”。
1.3 设计原则:针对接口\超类型编程,而不是针对实现编程
2.设计鸭子的行为
比如:需要产生一个新的绿头鸭实例,并指定特定“类型”的飞行行为给他,干脆顺便让鸭子的行为可以动态地改变好。
换言之,应该在鸭子类中包含设定行为的方法,这样可以在“运行时”动态地“改变”绿头鸭的飞行行为。
设计原则:针对接口编程,而不是针对实现编程
利用接口代表每个行为,比如:FlyBehavior与QuackBehavior,而行为的每个实现都将实现其中的一个接口。
因此,鸭子类不会负责实现Flying和Quacking接口,而是由制造的其他类专门实现FlyBehavior和QuackBehavior,这种称之为“行为”类。由行为类而不是Duck类来实现行为接口。
对比以前的做法:行为来自Duck超类的具体实现,或者继承某个接口并由子类自行来实现。这两种做法都是依赖于“实现”。我们被实现绑的死死的,除非写更多代码,否则没办法更改行为。
新的设计中,鸭子的子类使用接口(FlyBehavior与QuackBehavior)所表示的行为,注:此处是使用而非实现,是使用现成接口表示的行为,所以实际的“实现”不会被绑死在鸭子的子类中。(换言之,特定的具体行为编写在实现了FlyBehavior与QuackBehavior的类中)。
“针对接口编程”真正的意思是“针对超类型编程”
针对超类型编程,不仅限于是对interface,关键是在于多态的使用。
利用多态,程序可以针对超类型编程,执行时会根据实际状况执行到真正的行为,不会被绑死在超类型的行为上----动态分派,运行时选择实际对象。
“针对超类型编程”,也就是说:变量的声明类型应该是超类型,通常是一个抽象类或者是一个接口。
如此,只要是具体实现此超类型的类所产生的的对象,都可以指定给这个变量。这意味着,声明类时不用理会以后执行时的镇针对性。
关于多态的简单举例:
抽象类Animal,两个具体实现(Dog和Cat)继承Animal.
针对实现编程:
Dog d=new Dog();
d.bark();
针对接口\超类型编程:
Animal animal=new Dog();
d.bark();
知道对象是狗,但是现在利用的是animal进行多态的调用。 这样的好处是,子类实例化的动作不需要再代码中硬编码,是在“运行时指定具体实现的对象”。
实现鸭子的行为
有两个接口,FlyBehavior与QuackBehavior,还有他们对于的类,负责实现具体的行为:
这样设计的好处:
有了继承的“复用”好处,却没有继承所带来的的包袱。
- 这样设计,可以让飞线和呱呱叫的动作被其他的对象复用,因为这些行为已经与鸭子类无关。
- 并且可以新增一些行为,不会影响到既有的行为类,也不会影响“使用”到飞行行为的鸭子类。
整合鸭子的行为:
关键在于,鸭子现在会将飞行和呱呱叫的动作“委托”别人处理,而不是定义在Duck类(或子类)
内的呱呱叫和飞行方法。
做法是这样的:
1.首先,在Duck类中“加入两个实例变量”,分别为“flyBehavior”与“quackBehavoir”,声明为接口类型(而不是具体类实现类型)。每个鸭子对象都会动态地设置这些变量以在运行时引用正确的行为类型(例如:FlyWithWings、Squcak等)。
用两个相似的方法performFly()和performQuack()取代Duck类中的fly()与quack()。(稍后,你就知道为什么)
2.现在,来实现performQuack():
public class Duck{
QuackBehavoir quackBehavior;
//...
public void performQuack(){
quackBehavoir.quack();
}
}
想进行呱呱叫的动作,Duck对象只要叫quackBehavior对象去呱呱叫即可。在这部分的代码中,不在乎quackBehavoir接口的对象到底是什么,我们只关心对象知道如何呱呱叫就够了。
3.现在来看“如何设定flyBehavior与quackBehavior的实例变量”。看看MallardDuck类(绿头鸭类):
public class MallardDuck extends Duck{
public MallardDuck(){
quackBehavior=new Quack();
flyBehavior=new FlyWithWings();
}
@Override
public void display() {
System.out.println("i am a real Mallard duck");
}
}
绿头鸭使用Quack类处理呱呱叫,所以当performQuack()被调用时,叫的职责被委托给Quack对象,而我们得到了真正的呱呱叫。Fly也是同理。
如果指定绿头鸭以其他形式叫,则在构造器中将其他行为的实例变量初始化复制到quackBehavior中即可。
待升级的地方:
这只是暂时的,在本书的后续内容中,有更多的模式可用,到时候就可以修正这一点。
目前的做法还是有弹性的,只是初始化实例变量的做法不够弹性罢了。
测试Duck的代码
Duck类:
public abstract class Duck {
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public Duck(){
}
public abstract void display();
public void performFly(){
flyBehavior.fly();
}
public void performQuack(){
quackBehavior.quack();
}
public void swim(){
System.out.println("All ducks float,even decoys!");
}
}
FlyBehavior接口与两个行为实现类:
public interface FlyBehavior {
public void fly();
}
/******************************************************/
public class FlyWithWings implements FlyBehavior{
@Override
public void fly() {
System.out.println("I am flying!!");
}
}
/*********************************************************/
public class FlyNoWay implements FlyBehavior{
@Override
public void fly() {
System.out.println("I can not fly");
}
}
QuackBehavior接口与三个行为实现类:
public interface QuackBehavior {
public void quack();
}
/***********************************************/
public class Quack implements QuackBehavior{
@Override
public void quack() {
System.out.println("Quack...");
}
}
/**************************************************/
public class MuteQuack implements QuackBehavior {
@Override
public void quack() {
System.out.println("Silence...");
}
}
/***************************************************/
public class Squeak implements QuackBehavior {
@Override
public void quack() {
System.out.println("Squeak");
}
}
测试代码:
public class MiniDuckSimulator {
public static void main(String[] args) {
Duck mallrad=new MallardDuck();
mallrad.performFly();;
mallrad.performQuack();
}
}
运行代码:
动态设定行为:
注:纠正思想误区:代码便于修改、可维护性强等,都是指开发者对代码进行维护时,便于编写且易维护
1.在Duck类中加入两个新方法:
public void setFlyBehavior(FlyBehavior fb){
flyBehavior=fb;
}
public void setQuackBehavior(QuackBehavior qb){
quackBehavior=qb;
}
通过这个,可以“随时”调用这两个方法改变鸭子的行为。
可以在实际调用对象的时候,直接通过这两个方法对鸭子的行为作出设定。
2.制造一个新的鸭子类型:模型鸭(ModelDuck)
public class ModelDuck extends Duck{
public ModelDuck() {
flyBehavior=new FlyNoWay();//一开始,设定的模型是不会飞的
quackBehavior=new Quack();
}
@Override
public void display() {
System.out.println(" i am a model duck");
}
}
3.建立一个新的FlyBehavior类型
public class FlyRocketPowered implements FlyBehavior {
@Override
public void fly() {
System.out.println("i am flying with a rocket");
}
}
4.改变测试类,加上模型鸭,并使模型鸭具有火箭动力
public class MiniDuckSimulator {
public static void main(String[] args) {
Duck model=new ModelDuck();
model.performFly();
model.setFlyBehavior(new FlyRocketPowered());
model.performFly();
}
}
在运行时想改变鸭子的行为,只需调用鸭子的setter方法就可以 。
封装行为的大局观
下面看看整体的格局:
我们描述事情的方式也有所改变。不再把鸭子的行为说成是“一组行为”,我们把行为说成是“一族算法”。 想想看,在SimUDuck的设计中,算法代表鸭子能做的事(不同的叫法和飞行法),这样的做法也能很容易地用于用一群类计算不同州的销售税金。
注:确实如此,算法往往是执行相应的功能,在复杂的工程,如机器人的路径规划里面,算法是表述的机器人的行为或者功能。
1.4 设计原则3-多用组合,少用继承
“有一个”关系相当有趣:每一鸭子都有一个FlyBehavior和一个QuackBehavior,好将飞行和呱呱叫委托给它们代为处理。
当你将两个类结合起来使用,如同本例一般,这就是组合(composition)。这种做法和“继承”不同的地方在于,鸭子的行为不是继承来的,而是和适当的行为对象“组合”来的。
这是一个很重要的技巧。其实是使用了我们的第三个设计原则:
设计原则:多用组合,少用继承。
如此,使用组合建立系统具有很大的弹性,不仅可将算法族封装成类,更可以“在运行时动态地改变行为”,只要组合的行为对象符合正确的接口标准接口。
2.策略设计模式
上面这些设计原则的组合和背后的思想就是 策略设计模式
策略模式定义了算法族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
使用算法的客户:指的就是使用我们设计的类模块的开发者,就类似于开源中的调包,为了让使用开源包中的客户更方便的使用与设置,则就用策略模式,将算法族分别封装,易于替换,就像上面的setXXX的代码例子。
3.设计模式的好处
3.1 使用共享词汇更直白的描述设计模式的思路
这两人点的餐有何不同?其实没有差异,都是一份单,只是Alice讲话的长度多了一倍,而且快餐店的厨师已经感到不耐烦了。
什么是Flo有的,而Alice没有?答案是,Flo和厨师之间有“共享的词汇”,Alice却不懂这些词汇。共享的词汇不仅方便顾客点餐,也让厨师不用记太多事,毕竟这些餐点模式都已经在他的脑海中了呀!
设计模式让你和其他开发人员之间有共享的词汇,一旦懂得这些词汇,和其他开发人员之间沟通就很容易,也会促使那些不懂的程序员想开始学习设计模式。设计模式也可以把你的思考架构的层次提高到模式层面,而不是仅停留在琐碎的对象上。
简单理解,就是某种好的写法有专门的设计模式名字与之对应,则与其他开发者交流的时候,直接说XX模式即可,而无需将XX模式的编写思路具体说出来。
书中的P63-64就再说用 共享词汇 来描述设计模式的 彩虹屁。
3.2 怎么使用设计模式
设计模式不会直接进入你的代码中,而是先进入你的“大脑”中。一旦你先在脑海中装入了许多关于模式的知识,就能够开始在新设计中采用它们,并当你的旧代码变得如同搅和成一团没有弹性的意大利面一样时,可用它们重做旧代码。