PS:如下内容纯属个人想法,有问题,请轻拍!
早些时候在坛子里看到了这样一个帖子(具体帖子链接找不到了)。内容是这样的。有一个需求:要用*打印出三行的一个三角形。具体怎么实现。相信看到这个题目,有语言基础的人,第一反应就是两个循环,搞定。作者给出的答案如下:
System.out.println(" *");
System.out.println(" ***");
System.out.println("*****");
第一眼看了可能很好笑,但看了作者的解释的确如此。用户永远是bt的,需求永远是变化的。
而最近在跟chjavach的设计模式,个人感觉桥接模式写得最好,我看了以后思路很清晰,其他模式看完没有那么清晰的感觉。而
简单工厂里的这一幅图则是醍醐灌顶啊。让我终于知道了Factory的存在理由了。这可能和个人的开发经历有关,一般都是一个人包揽前后台,而不是分工合作的原因吧。
设计模式说到底是用来封装变化的,就对上面的题目来看,如果需求没有变化,那么上面的实现是完全可以的。而当需求变化的时候再去重构吧---是重构,不是简单的修改。好像是Bob的《敏捷软件开发:原则、模式与实践》这本书里看到的吧!不要让子弹击中你两次。也就是说第一次以最快实现方式实现需求,当需求更改时,进行重构。
前段时间看了不少书,数据结构,算法,python,c,c++,gog还有杂七杂八的书籍,当然都没怎么深究了。然后某天晚上睡觉时就想了不少问题!需求是不断变化的。那什么时候使用设计模式?怎么就知道要使用什么样的设计模式了?如果像敏捷开发所说的,重构出模式,什么时候重构?有多大风险?是否不同的语言对变化的解决方案不同呢?如果相同,各位大师发明这么多语言干嘛?如果不同,怎么个不同法呢?
相信看过HeadFirst设计模式的人对第一章应该印象都比较深刻吧!这就是一个由需求变更而导致重构进而演化出了策略模式的例子。
这里引用一下他的例子,不过从更原始的模型开始。一开始只有一只鸭子。它能quake,swing,display。如下图所示:
代码如下:
public class Duck {
public String quack() {
return "GaGa";
}
public String swing() {
return "Swing";
}
public String display() {
return "Duck Display";
}
}
测试一下:
public class DuckTest {
private Duck duck ;
@Before
public void setUp() throws Exception {
duck = new Duck();
}
@After
public void tearDown() throws Exception {
}
@Test
public void testQuack() throws Exception {
assertEquals("GaGa",duck.quack());
}
@Test
public void testSwing() throws Exception {
assertEquals("Swing",duck.swing());
}
@Test
public void testDisplay() throws Exception {
assertEquals("Duck Display",duck.display());
}
}
这里的Duck就是server端的代码,测试类则相当于client端的代码。
这段代码肯定是没有问题的,太简单了。如果所有的项目都这么简单,大家都笑了。
接着出现了第二只鸭子,它是只红头鸭子,它也能quack,swing和display.不同的地方是红头鸭子的display显示的是"RedDuck Display",那么这里能想到的就是继承了,让RedDuck继承Duck,继而覆写Duck的display方法就可以了。
然后又是,GreenDuck啦之类的,类图变成这样。
以后如果要添加鸭子,只要添加相应的鸭子类,继承Duck,覆写Duck的display方法就可以了。
可以看出,这样的设计对这样的需求是符合要求的。符合开闭原则---对修改关闭,对扩展开放。我们来看一下,这段代码怎么对修改关闭,对扩展开放了。在server端,当需要添加鸭子的时候,只需要继承Duck即可。不需要修改现有的任何代码。有人就问了,那client端呢?不是要改代码吗?(以前我也有这样的疑问)。同样是看了上面那幅Factory的图我豁然开朗。当区分开了client和server端后,知道了开闭原则是针对server端的。对于client端,你要调用server端新添加的鸭子当然要改源代码了,不然怎么调用?当然server端可以封装个Factory提供出来。源代码多多少少都是需要修改的。
对于这样的设计呢!OO语言都是没什么太大的区别的,C++,Ruby实现都很类似了。
第一个平衡点:
这里可能会有一些声音,说对于client来说,调用Duck的时候需要new才可以,所以要个Factory。这里可能就是斟酌的地方了。上面已经说了,模式是用来封装变化的。如果这几个Duck类都不会变化,那么client使用new,也没什么问题。而问题是你没办法保证这些Duck不会发生变化。从另一个角度来看,你也不能保证这些Duck一定就会发生变化,如果所有的地方不分青红皂白全用Factory,那么就会Factory类爆炸了吧。而如果都不用Factory类,如果Duck类发生了变化,那么当类的数量变化得很多时,修改就是个噩梦了吧。
第二个平衡点:
接着呢!出现了WoodDuck了,他是不会quack的。继续上面的方式--继承。覆写quack方法,空实现即可。再来一个PaperDuck呢?继续继承?空实现?这里就出现了重复了。而这里应该又是一个平衡点了。HeadFirst是假设此类不会quack的Duck越来越多的话,重复代码就越来越多了,继而进行了重构。我觉得这里有几个问题
1.此处完全是在假设的前提下进行重构的。如果类没继续增加呢?
2.如果要重构,到底多少个类似的Duck出现时需要重构呢?
3.假设根据DRY原则,出现第二个的时候就进行重构是否有些劳师动众?如果Duck已经继承了100甚至更多的类了,而且类运行得很好,此时出现了WoodDuck和PaperDuck进行重构的话,风险有多大呢?
4.有测试呢!你能保证测试覆盖全面吗?!
我们继续往下看!假设现在已经有100个类似RedDuck,GreenDuck的类继承了Duck了,测试类覆盖完全。此时出现了WoodDuck和PaperDuck类,这个时候再看HeadFirst里面的关于抽象出接口的实现方式,应该一眼就看出他的弊端了。抽象出一个Quackable接口,能Quack的就实现这个接口,不能Quack的就不实现这个接口。好吧,现在有100只鸭子能Quack,2只不能Quack,你如此重构看看。多了100个重复(谁让接口不能实现方法呢!)。。。知道什么叫吃力不讨好了吗?
这时候C++笑了,让你不能多继承。看我,直接抽出Quack父类,谁能Quack就继承Quack。不能Quack的就不继承。
Ruby也笑了,直接将quack放到Quack模块里面去,谁能quack就mixin Quack模块呗。
再回到Java,这里就开始关注变化点!很明显,这里的不稳定因素是quack,有的鸭子能quack有的则不能,我们则把quack抽出来,独立为一个类。当需要quack的时候就设置这个quack即可。这就是策略模式。
第三个平衡点:
策略模式应该也分为两种,我自己把它称作server端的策略和client端的策略。server端的策略就是将quack实例化在了相应的duck内部,client端直接实例化即可。而client端的策略就是在client端自己设置相应的quack到duck中去。很明显,client端的策略更灵活。但是这里不适用。看看这里已经有100多个类了,如果每个类都修改为client端的策略模式,那么修改量太大,收效也不明显。当然也可以混合使用,默认提供了server端的策略,也提供client端的策略,供client端灵活调用。
对于server端的策略实现,我们和c++,ruby的实现比较一下。假如,GreenDuck的quack需要修改了,策略模式是将duck内的quack实例替换掉。c++是将其quack父类修改掉(这里的父类肯定也是有个继承关系的,都有共同的父类,否则client端怎么调用呢?)。而ruby呢,直接添加一个新的quack模块,替换原来那个模块就可以了。
所以,c++的多继承,ruby的mixin也同样的解决了问题。但是,c++和ruby从语言特性级别解决了java需要使用设计模式才能解决的问题。当然了,c++,ruby也是能实现策略模式的。
总结:
1. 模式也不是乱用的。对于第一个平衡点,可能会导致Factory爆炸。对于第二个平衡点,可能会加大工作量和风险。怎么平衡,看项目,看需求,看个人经验。
2. 模式需要结合实际情况。像上面的例子,c++,ruby从语言级别就可以解决问题了,不需要再升级到模式的级别。
前段时间看了一本书《怪诞行为学》,里面提到了一些关于暗示的例子,不知道用在这里合不合适。例子是这样的,一家出版社网站打印了如下的书籍价格:《书籍A》pdf版本:$59,《书籍A》实体书:$120,《书籍A》pdf+实体书:$120。多少人会买$120的实体书呢?选项2暗示了你,选项3比选项1要划算!而在这里,当我们学完OO,书上说继承怎么样怎么样好的时候,我们的心里其实已经受到了影响,有问题就继承。再学习了模式后,又一次被暗示了,有问题找模式。继承和模式不过是一种结构而已,有适用的地方,具体哪里适用,需要我们来掌握。怎么掌握?经验。。。。也许这就是毕业生和资深开发人员的本质区别吧。