设计模式这一部分初期主要是对HeadFirst Design Pattern 这本书的内容进行总结的,在学习完基本的设计模式之后,我会间断性地依据实际开发中的源码作为实例对设计模式这一方面的内容进行扩展和补充。
为什么要有设计模式设计模式要遵循的最基本原则策略模式Java开发人员对于OO(面向对象)设计这一理念必然是非常熟悉了,提起OO设计,大部分人都可以不经思考就说出封装、继承、多态这几种OO设计中的特性。然而在实际的软件开发中,单只知道这几种特性是远远不够的。 那么这里就引申出设计模式了:设计模式就是软件开发人员在实际的OO开发中总结出来的隐含经验,这些经验有助于设计出弹性、可复用、维护性好的系统。
开闭原则: 一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。 (Softeware entities like classes,modules and functions should be open for extension but closed for modifications.) 这个设计原则是Java里面最基础的设计原则。因为它的存在意味着代码的设计必须要低耦合。如果在一个耦合度很高的软件系统中,对功能的扩展和修改很难保证不修改已有软件实体中的内容。 开闭原则存在的好处是显而易见的,它防止了对已有代码的修改,增加了系统的鲁棒性。而且,开闭原则只需要对新增代码进行单元测试就可以了,减少了测试的工作量。
HeadFirst Design Pattern 书中第一个模式就是策略模式,就以书中的实例来进行分析。
一个游戏公司在创建一个以鸭子模型为主题的游戏,鸭子有很多种类,有红毛鸭、绿毛鸭等等,他们包含了游泳、叫唤(嘎嘎)、飞行等功能。 然而,事情不是这么简单。除了动物鸭子外,游戏公司还别出心裁的加入了橡皮鸭的功能,然而橡皮鸭叫唤的方式为“啊啊”(参考那只惨叫鸡),然后他还告诉程序员,有可能会为了增加趣味性,在游戏中加入一些非同寻常的鸭子,比如橡皮鸭(不会叫唤,不会飞)或者一些别的类型的鸭子。
这个实例代表着实际软件开发中经常会出现的情况,那就是”change”,你不知道开发经理什么时候会给你加个什么千奇百怪的需求,这时候你就很苦逼了。 实际上,开发一份代码可能需要半年,而维护一份代码的时间可能会超过5年甚至更多。那么维护过程中,难免会碰到代码需要”change”的情况的,那么我们应该怎么办呢?还是就事论事,以上文中的鸭子作为例子来分析本系列的第一种设计模式吧。
首先我们第一反应会想到先新建一个Duck类,里面声明swim, quack, fly等方法,然后对于橡皮鸭、木头鸭等,将它们声明为Duck类的子类就OK了。这种设计的代码是这么的。
public class Duck{ public void swim(){ System.out.println("Look, I can swim."); } public void quack(){ System.out.println("ga ~ ga ~ "); } public void fly(){ System.out.println("HaHa, I can fly"); } } public class RubberDuck extends Duck{ public void quack(){ System.out.println("a ~ a ~"); } public void fly(){ // } } public class WoodDuck extends Duck{ pubic void quack(){ //nothing } public void fly(){ //nothing } }上面的代码会造成的问题是,假如代码需要扩展(比如再加入有特殊飞行行为的鸭子),那么Duck父类的方法需要不断被override,这是很麻烦的一种行为(这其实是不符合里氏法则的,这里我也还没有搞懂,以后再分析)。而且假如开发经理又说可以在木头鸭上加一个火箭来更改其飞行行为的话,那么WoodDuck这个类是需要被修改的,这就不符合最基本的开闭原则了。 同理,如果将fly()方法从超类中取出来,并将其声明为一个flyable()接口,这种行为也是不对的。因为implements这个接口后,那么在fly()行为变更时,还是涉及到在具体实现此接口的类中去作修改的。
策略模式的设计法则就是:将Duck类中的需要变化的部分(fly(), quack())方法抽取出来,分别将他们放在自己的行为类中。这样就降低Duck类和易变行为之间的耦合了。 代码如下:
//定义飞行接口,叫唤接口 public interface FlyBehavior{ public void fly(); } public interface QuackBehavior{ public void quack(); } //定义不同的飞行、叫唤的行为类 public class Quack implements QuackBehavior{ public void quack(){ System.out.println("ga ~ ga ~ "); } } public class Squeak implements QuackBehavior{ public void quack(){ System.out.println("a ~ a ~"); } } public class Mute implements QuackBehavior{ public void quack(){ //nothing } } public void FlyWithWings implements FlyBehavior{ public void fly(){ System.out.println("HaHa, I can fly."); } } public class FlyNoWay implements FlyBehavior{ public void fly(){ //nothing } } //定义Duck基类,将不变的方法swim()放在里面 public class Duck{ public FlyBehavior flyBehavior; public QuackBehavior quackBehvior; public void swim(){ System.out.println("Look, I can swim."); } public void performFly(){ flyBehavior.fly(); } public void performQuack(){ quackBehvior.quack(); } }假如我们要实现一个正常鸭子的类,代码如下:
public class NormalDuck extends Duck{ public NormalDuck(){ flyBehavior = new FlyWithWings(); quackBehvior = new Quack(); } }测试结果为:
然后测试木头鸭子类,代码为:
public class WoodDuck extends Duck{ public WoodDuck(){ flyBehavior = new FlyNoWay(); quackBehvior = new Mute(); } }测试结果为:
这就很明白了,在Duck类中只声明了变量为接口类型,然后在具体的实现中,才将变量指向了具体的实现(具体鸭子是怎么飞的,怎么叫的),这就充分的利用了多态的特性。
以上的分析也可以总结为:
-针对接口编程,而非针对实现编程。 -多用组合,少用继承。
但是上述实例中其实还是有不足的,在WoodDuck类中,有这么一段:
flyBehavior = new FlyNoWay(); quackBehvior = new Mute();这里面还是针对实现编程的,当这个木头鸭子装上火箭,fly()方法需要改变时,那么这个WoodDuck类可能就又得改成这样:
flyBehavior = new FlyWithRocket();那么其实这里还是不符合开闭原则的,HeadFirst Design Pattern 书中也对这里留下了一些悬念,并表明后续会修改,所以让我们拭目以待吧。