1. 模式定义
把会变化的内容取出并封装起来,以便以后可以轻易地改动或扩充部分,而不影响不需要变化的其他部分;
2.模式本质:
少用继承,多用组合,简单地说就是:固定不变的信息封装在一个类中,变化的信息我们使用接口,抽象定义,那么使用的时候:继承不变的,自定义实现变化的那 部分!
3.举例分析:
示例:
一个鸭子类型: 抽象类型
/*抽象的鸭子类*/
1 public abstract class Duck { 2 //所有的鸭子均会叫以及游泳,所以父类中处理这部分代码 3 public void quack() { 4 System.out.println("嘎嘎嘎....."); 5 } 6 7 public void swim() { 8 System.out.println("游啊游......."); 9 } 10 11 //因为每种鸭子的外观是不同的,所以父类中该方法是抽象的,由子类型自己完成。 12 public abstract void display(); 13 }
具体鸭子的继承子类:
1 public class MallardDuck extends Duck { 2 //野鸭外观显示为绿头 3 public void display() { 4 System.out.println("我是绿头鸭子......"); 5 } 6 } 7 8 public class RedHeadDuck extends Duck { 9 //红头鸭显示为红头 10 public void display() { 11 System.out.println("我是红头鸭子......"); 12 } 13 } 14 15 public class RubberDuck extends Duck { 16 //橡皮鸭叫声为嘎嘎嘎叫,所以重写父类以改写行为 17 public void quack() { 18 System.out.println("嘎嘎嘎......."); 19 } 20 21 //橡皮鸭显示为黄头 22 public void display() { 23 System.out.println("我是黄头鸭子......."); 24 } 25 }
/***************************【分析说明】***************************************/ 上述代码,初始实现得非常好。现在我们如果给Duck.java中加入fly()方法的话,那么在子类型中均有了该方法,于是我们看到了 会飞的橡皮鸭子,你看过吗? 当然,我们可以在子类中通过空实现重写该方法以解决该方法对于子类型的影响。但是父类中再增加其它的方法呢? 通过继承在父类中提供行为,会导致以下缺点: a. 代码在多个子类中重复; b. 运行时的行为不容易改变; c. 改变会牵一发动全身,造成部分子类型不想要的改变; /******************************************************************************/
好啦,还是刚才鸭子的例子,你也许想到使用接口,将飞的行为、叫的行为定义为接口,然后让Duck的各种子类型实现这些接口。
这时侯代码类似于:
1 public abstract class Duck { 2 //将变化的行为 fly() 以及quake()从Duck类中分离出去定义形成接口,有需求的子类中自行去实现 3 4 public void swim() { 5 System.out.println("游啊游......."); 6 } 7 8 public abstract void display(); 9 } 10 11 //变化的 fly() 行为定义形成的接口 12 public interface FlyBehavior { 13 void fly(); 14 } 15 16 //变化的 quack() 行为定义形成的接口 17 public interface QuackBehavior { 18 void quack(); 19 } 20 21 //野鸭子会飞以及叫,所以实现接口FlyBehavior, QuackBehavior 22 public class MallardDuck extends Duck implements FlyBehavior, QuackBehavior{ 23 public void display() { 24 System.out.println("我是绿头鸭......."); 25 } 26 27 public void fly() { 28 System.out.println("Fly."); 29 } 30 31 public void quack() { 32 System.out.println("嘎嘎嘎:绿鸭子叫........"); 33 } 34 } 35 36 //红头鸭子会飞以及叫,所以也实现接口FlyBehavior, QuackBehavior 37 public class RedHeadDuck extends Duck implements FlyBehavior, QuackBehavior{ 38 public void display() { 39 System.out.println("我是红头鸭......"); 40 } 41 42 public void fly() { 43 System.out.println("飞呀飞......."); 44 } 45 46 public void quack() { 47 System.out.println("嘎嘎嘎:红鸭子叫"); 48 } 49 } 50 51 //橡皮鸭不会飞,但会嘎嘎嘎叫,所以只实现接口QuackBehavior 52 public class RubberDuck extends Duck implements QuackBehavior{ 53 //橡皮鸭叫声为嘎嘎嘎叫 54 public void quack() { 55 System.out.println("嘎嘎嘎:橡皮鸭叫......"); 56 } 57 58 //橡皮鸭显示为黄头 59 public void display() { 60 System.out.println("我是黄头鸭......"); 61 } 62 }
分析:
上述代码虽然解决了一部分问题,让子类型可以有选择地提供一些行为(例如 fly() 方法将不会出现在橡皮鸭中).但我们也看到,野鸭子MallardDuck.java和红头鸭子RedHeadDuck.java的一些相同行为代码不能得到重复使用。很大程度上这是从一个火坑跳到另一个火坑。
在一段程序之后,让我们从细节中跳出来,关注一些共性问题。不管使用什么语言,构建什么应用,在软件开发上,一直伴随着的不变的真理是:需要一直在变化。不管当初软件设计得多好,一段时间之后,总是需要成长与改变,否则软件就会死亡。
我们知道,继承在某种程度上可以实现代码重用,但是父类(例如鸭子类Duck)的行为在子类型中是不断变化的,让所有子类型都有这些行为是不恰当的。我们可以将这些行为定义为接口,让Duck的各种子类型去实现,但接口不具有实现代码,所以实现接口无法达到代码复用。这意味着,当我们需要修改某个行为,必须往下追踪并在每一个定义此行为的类中修改它,一不小心,会造成新的错误。
设计原则:
设计原则:
把应用中变化的地方独立出来,不要和那些不需要变化的代码混在一起。这样代码变化引起的不经意后果变少,系统变得更有弹性。
按照上述设计原则,我们重新审视之前的Duck代码。
1) 分开变化的内容和不变的内容
Duck类中的行为 fly(), quack(), 每个子类型可能有自己特有的表现,这就是所谓的变化的内容。
Duck类中的行为 swim() 每个子类型的表现均相同,这就是所谓不变的内容。
我们将变化的内容从Duck()类中剥离出来单独定义形成接口以及一系列的实现类型。将变化的内容定义形成接口可实现变化内容和不变内容的剥离。其实现类型可实现变化内容的重用。这些实现类并非Duck.java的子类型,而是专门的一组实现类,称之为"行为类"。由行为类而不是Duck.java的子类型来实现接口。这样,才能保证变化的行为独立于不变的内容。
于是我们有:
1 //变化的 fly() 行为定义形成的接口 2 public interface FlyBehavior { 3 void fly(); 4 } 5 6 //变化的 fly() 行为的实现类之一 7 public class FlyWithWings implements FlyBehavior { 8 public void fly() { 9 System.out.println("飞呀飞......"); 10 } 11 } 12 13 //变化的 fly() 行为的实现类之二 14 public class FlyNoWay implements FlyBehavior { 15 public void fly() { 16 System.out.println("我翅膀受伤了,飞不了......"); 17 } 18 }
1 //变化的 quack() 行为定义形成的接口 2 public interface QuackBehavior { 3 void quack(); 4 } 5 6 //变化的 quack() 行为实现类之一 7 public class Quack implements QuackBehavior { 8 public void quack() { 9 System.out.println("嘎嘎嘎......"); 10 } 11 } 12 13 //变化的 quack() 行为实现类之二 14 public class Squeak implements QuackBehavior { 15 public void quack() { 16 System.out.println("吱吱吱......."); 17 } 18 } 19 20 //变化的 quack() 行为实现类之三 21 public class MuteQuack implements QuackBehavior { 22 public void quack() { 23 System.out.println("哑巴,不会叫......."); 24 } 25 }
通过以上设计,fly()行为以及quack()行为已经和Duck.java没有什么关系,可以充分得到复用。而且我们很容易增加新的行为, 既不影响现有的行为,也不影响Duck.java。但是,大家可能有个疑问,就是在面向对象中行为不是体现为方法吗?为什么现在被定义形成类(例如Squeak.java)?在OO中,类代表的"东西"一般是既有状态(实例变量)又有方法。只是在本例中碰巧"东西"是个行为。既使是行为,也有属性及方法,例如飞行行为,也需要一些属性记录飞行的状态,如飞行高度、速度等。
整合变化的内容和不变的内容:
1 public abstract class Duck { 2 //将行为类声明为接口类型,降低对行为实现类型的依赖 3 FlyBehavior flyBehavior; 4 QuackBehavior quackBehavior; 5 6 public void performFly() { 7 //不自行处理fly()行为,而是委拖给引用flyBehavior所指向的行为对象 8 flyBehavior.fly(); 9 } 10 11 public void performQuack() { 12 quackBehavior.quack(); 13 } 14 15 public void swim() { 16 System.out.println("游啊游......"); 17 } 18 19 public abstract void display(); 20 }
Duck.java不关心如何进行 fly()以及quack(), 这些细节交由具体的行为类完成。
1 public class MallardDuck extends Duck{ 2 public MallardDuck() { 3 flyBehavior=new FlyWithWings(); 4 quackBehavior=new Quack(); 5 } 6 7 public void display() { 8 System.out.println("Green head."); 9 } 10 }
测试类:
1 public class DuckTest { 2 public static void main(String[] args) { 3 Duck duck=new MallardDuck(); 4 duck.performFly(); 5 duck.performQuack(); 6 } 7 }
4.思想重点:
(1)策略思想:
继承,可以实现静态代码的复用;
组合,可以实现代码的弹性维护;
使用组合代替继承,可以使代码更好地适应软件开发完后的需求变化。
(2)策略模式的本质:
少用继承,多用组合