需求
展示一只鸭子,鸭子会叫,会游泳,还有不同的外观。
初步设计
public abstract class Duck
{
/**
* 所有的鸭子都会有外观,只是每只鸭子的外观都可能不一样
*/
public abstract void display();
public void swim()
{
System.out.println("鸭子会游泳");
}
public void quack()
{
System.out.println("鸭子会呱呱叫");
}
}
看起来似乎不错,�鸭子只要继承自 Duck,然后实现自己的外观即可了。
一段时间后,需求改了,现在需要鸭子会飞,叫声也可能有不一样的
。
于是就在父类中加了 fly 的方法
public abstract class Duck
{
public void fly()
{
System.out.println("让鸭子飞");
}
// 省略其他的
}
叫声在这里不用修改,因为子类只需要覆盖quack方法即可。
然后可怕的事情发生了,现在发现,橡皮鸭子也会飞了,原本是不应该会飞的。
可见为了复用而使用继承会导致问题:原本某些子类不需要的能力被父类强行赋予了
思考以上代码会导致的问题:
- 代码在多个子类中重复(不会飞的鸭子也有了飞的方法)
- 牵一发动全身,后续需要加入新功能,会导致所有的子类都受影响
- 不能动态改变鸭子的行为,比如让他竖着飞或横着飞,也很难知道鸭子的全部行为
既然继承�不好,那就使用接口怎么样?
考虑到“飞”,“叫声”行为是�可变的,因此封装成接口,让需要飞和叫的鸭子实现这些接口不就好了吗?来试试!
/**
* 所有飞行行为类必须实现的接口
*/
public interface FlyBehavior
{
public void fly();
}
/**
* 所有鸭叫行为必须实现的接口
*/
public interface QuackBehavior
{
public void quack();
}
那么鸭子父类就变成:
public abstract class Duck
{
/**
* 所有的鸭子都会有外观,只是每只鸭子的外观都可能不一样
*/
public abstract void display();
public void swim()
{
System.out.println("鸭子会游泳");
}
}
而需要叫声或者飞行行为的鸭子之类,就可以通过实现接口来完成,然后实现自己的飞和叫的行为,不管是横着飞竖着飞,你自己都可以决定。
但是也会导致以下问题:
- 如果需要修改行为,那么就要具体到每个鸭子子类的源码,一不小心可能就会出现问题。
- 代码无法复用,比如说,定义了 A 鸭子会�横着飞,,B 鸭子会横着飞,那么横着飞这个行为就会在 A 和 B 中重复。
软件开发的真理
需求永远在变
设计原则 1
找出可能变化的地方,并将可能会变化的独立出来,不要和不变的那些�代码混在一起。
也就是说:如果有新需求�来了,就会使某一块变化,那么就可以确定,这块是需要被�抽出来的。确保系统中某一部分的改变,不会导致影响到其他部分
设计原则 2
针对接口编程,而不是针对实现编程,是针对超类型编程,这里的接口不一定是语法意义上的接口。也就是利用多态。
OK �,现在可以将飞和叫声这两个会变化的独立出来了
我们可以将飞这个行为归为一组,将叫声这个行为归为一组。它们将和鸭子类完全隔离。
/**
* 飞行行为的实现类,给不会飞的鸭子用
*/
public class FlyNoWay implements FlyBehavior
{
@Override
public void fly()
{
System.out.println("我不会飞");
}
}
/**
* 飞行实现类,用火箭来飞
*/
public class FlyRocketPowered implements FlyBehavior
{
@Override
public void fly()
{
System.out.println("我可以和火箭一起飞");
}
}
/**
* 飞行行为的实现,给真会飞的鸭子用
*/
public class FlyWithWings implements FlyBehavior
{
@Override
public void fly()
{
System.out.println("我会飞");
}
}
/**
* 声音实现类,给会吱吱叫的鸭子用
*/
public class Squeak implements QuackBehavior
{
@Override
public void quack()
{
System.out.println("我会吱吱叫");
}
}
/**
* 叫声实现类,给不会叫的鸭子用
*/
public class MuteQuack implements QuackBehavior
{
@Override
public void quack()
{
System.out.println("我不会叫");
}
}
整合鸭子的行为
�如何�让鸭子和他们的行为发生关联呢?我们的目的是行为可以被动态赋予,因此可以让行为成为鸭子的一个实例变量,暴露出相应的方法去触发行为。新的鸭子类如下:
/**
* 鸭子父类
*/
public abstract class Duck
{
// 所有鸭子子类都实现这两个接口类型
protected FlyBehavior flyBehavior;
protected QuackBehavior quackBehavior;
/**
* 所有的鸭子都会有外观,只是每只鸭子的外观都可能不一样
*/
public abstract void display();
// 委托给飞行行为类
public void performFly()
{
flyBehavior.fly();
}
// 委托给鸭叫声类
public void perfirmQuack()
{
quackBehavior.quack();
}
/**
* 只要是鸭子都会游泳
*/
public void swim()
{
System.out.println("所有的鸭子都会游泳");
}
/**
* 动态设定鸭子的飞行方式
*/
public void setFlyBehavior(FlyBehavior fb)
{
flyBehavior = fb;
}
/**
* 动态设定鸭子的叫声
*/
public void setQuackBehavior(QuackBehavior qb)
{
quackBehavior = qb;
}
}
测试
现在可以创建你想要的鸭子了。
/**
* 绿头鸭类,一继承了Duck类,从一开始就是会游泳的,而外观可以自己定义,飞行方式和叫声都可以根据需要自己定义
*/
public class MallardDuck extends Duck
{
public MallardDuck()
{
// 声明这个鸭子是飞行和叫声行为
flyBehavior = new FlyWithWings();
quackBehavior = new Quack();
}
@Override
public void display()
{
System.out.println("我是一只绿头鸭");
}
}
/**
* 创建一个模型鸭,一开始是不会飞的
*/
public class ModelDuck extends Duck
{
public ModelDuck()
{
flyBehavior = new FlyNoWay();
quackBehavior = new Quack();
}
@Override
public void display()
{
System.out.println("我是一只模型鸭");
}
}
public static void main(String[] args)
{
// 创建一个绿头鸭
Duck mallardDuck = new MallardDuck();
// 调用继承而来的performFly方法,委托给FlyBehavior对象处理
mallardDuck.performFly();
// 调用继承而来的performQuack方法,委托给QuackBehavior对象处理
mallardDuck.perfirmQuack();
// 创建一个模型鸭
Duck modelDuck = new ModelDuck();
modelDuck.performFly();
modelDuck.perfirmQuack();
// 动态设定鸭子的飞行方式,这里让鸭子和火箭一起飞
modelDuck.setFlyBehavior(new FlyRocketPowered());
// 这样就可以动态的设定鸭子的行为了,如果绑定在鸭子类中,就无法这样做
modelDuck.performFly();
}
设计原则 3
多用组合,少用继承。
上面可以看到的是,鸭子的行为并不是通过继承而来的,而是通过组合而来的。
鸭子的飞行行为可以看做是一族算法,叫声行为也可以看做是一族算法,在一族算法内,族内的行为是可以互相替换的,比如一族飞行算法,横着飞和竖着飞是可以互相替换的。
策略模式的定义:
定义了算法族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。