# 策略模式
# 现实案例:
- 想要用 OO 技术,实现一堆鸭子对象;
- 鸭子有
fly
(飞) 和quack
(叫) 两种行为; - 每个鸭子的行为可能都各不相同;
- 也并非所有的鸭子都会飞,都会叫(例如:木头鸭子);
# 方案一:❌ 用继承来实现:
- 实现一个父类
Duck
,里面定义好fly
和quack
方法; - 再让各个鸭子子类去继承
Duck
父类的方法; - 根据鸭子行为的不同,再在子类里面去覆盖
fly
和quack
方法;
// 父类
public class Duck {
public void quack() {
System.out.println("呱呱叫");
}
public void fly() {
System.out.println("飞");
}
}
// 子类
public class DisabledDuck extends Duck {
// 根据子类的要求,覆盖父类的方法
@Override
public void quack() {
System.out.println("不会叫");
}
@Override
public void fly() {
System.out.println("不会飞");
}
}
# 方案一的问题:
- 在父类里定义的
fly
和quack
方法并不是每个子类都具备的。而且各个子类之间还有行为差异; - 通过继承,会让一些子类具有不适合他们的方法,还需后期重写覆盖,增加了代码量;
- 如果有子类之间有相同的行为,但又和从父类继承来的行为不一致。就只能在多个子类各自重写相同的方法。这样使相同的代码在多个子类中重复;
- 并没有提供一个方法,来让子类的行为可以在运行时修改。(实例化完子类,但是又想更改实例的行为);
- 很难知道所有鸭子的全部行为。只能一个一个子类地去看;
- 如果以后父类有新的特性,还需要一个一个子类的去检查,是否新特性适合子类。不适合的话,还要在子类进行变动;
- 利用继承,父类和子类的耦合度太高;
# 方案二:❌ 利用接口来实现:
- 我们可以把
fly
和quack
方法从父类中提取出来; - 各自放进一个
Flyable
和Quackable
接口; - 然后在子类中按需求各自实现这些接口;
public class Duck {
}
public interface Flyable {
public void fly();
}
public interface Quackable {
public void quack();
}
public class MallardDuck extends Duck implements Flyable, Quackable {
@Override
public void fly() {
//实现该方法
}
@Override
public void quack() {
//实现该方法
}
}
# 方案二的问题:
- 虽然通过接口,不会让子类去继承不需要的特性;
- 但是,因为接口要在子类中去实现。那么每个子类都需要单独写一份
fly
和quack
方法; - 即使有的鸭子之间的行为是相同的,它们的代码也不能复用;
- 最差的情况是,每个子类之间的行为都是相同的。那么如果这个行为的代码需要修改,就要到每一个子类里面都修改一遍;
# 方案三:✅ 采用策略模式,针对接口编程:
- 在这个问题中,
fly
和quack
行为是根据鸭子子类的不同而 “变化” 的。而鸭子其他的东西目前是 “不变的”。一个设计原则是 “把变化的部分从不变的部分抽离出来”; - 我们分别给
fly
和quack
行为各创建一组类。每一组类将实现各自的动作; - 先创建
FlyBehavior
和QuackBehavior
接口/父类; - 之后再写一堆子类去分别实现这两个接口/父类;
- 在 Duck 父类里,加上两个实例变量
flyBehavior
和quackBehavior
,它们的类型分别是上面 👆 定义的接口/父类; - 在鸭子子类实例化的时候,我们再将具体的行为实例,赋值给上面的两个实例变量;
- 这样一来每个鸭子子类实例,都有了各自的行为了;
- 我们甚至可以给两个实例变量设置 setter 函数,以让开发者在运行时,动态的修改行为类;
// 定义两个行为的接口
public interface FlyBehavior {
public void fly();
}
public interface QuackBehavior {
public void quack();
}
// 为两个接口,各创建一组实现类
public class FlyNoWay implements FlyBehavior {
@Override
public void fly() {
//什么都不做,不会飞
System.out.println("不会飞");
}
}
public class FlyWithWings implements FlyBehavior {
@Override
public void fly() {
//实现了所有有翅膀的鸭子飞行行为。
System.out.println("可以飞");
}
}
public class Quack implements QuackBehavior {
@Override
public void quack() {
//实现呱呱叫的鸭子
System.out.println("呱呱叫");
}
}
public class Squeak implements QuackBehavior {
@Override
public void quack() {
//实现吱吱叫的鸭子
System.out.println("吱吱叫");
}
}
// Duck 父类
public class Duck {
// 分别对应两个行为类的实例
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
// setter, 用以改变行为类
public void setFlyBehavior(FlyBehavior flyBehavior) {
this.flyBehavior = flyBehavior;
}
public void setQuackBehavior(QuackBehavior quackBehavior) {
this.quackBehavior = quackBehavior;
}
// 调用行为实例的方法
public void performFly() {
flyBehavior.fly();
}
public void performQuack() {
quackBehavior.quack();
}
}
// 鸭子子类
public class MallardDuck extends Duck {
// 在创建实例的时候,分别给两个行为属性指定具体的行为类;
public MallardDuck() {
flyBehavior = new FlyWithWings();
quackBehavior = new Quack();
}
}
public class Test {
public static void main(String[] args) {
Duck duck = new MallardDuck();
duck.performFly(); // 可以飞
duck.setFlyBehavior(new FlyNoWay());
duck.performFly(); // 不会飞
duck.performQuack(); // 呱呱叫
}
}
# 总结:策略模式
- 策略模式定义了算法族,各个算法之间可以互相替换。此模式让算法的变化,独立于使用算法的客户;
- 在 👆 上面的例子中,我们各写了一组 fly 行为和 quack 行为的实现类,这两组行为就被称为 “算法族”。客户就是鸭子类,它里面会使用到具体的一个行为类;
- 之所以这样写,是因为鸭子的行为是会变化的。所以设计时,应该 “把会变化的部分抽取出来进行封装,以便以后可以轻易的改动和扩展,而不影响不会变化的部分”;
- 鸭子具体使用哪个行为,不在定义类的时候写,类里面只是定义了两个类型为算法族接口/父类的实例属性。具体的行为类型是在实例化的时候再决定。在设计时,应该 “针对超类型编程,而不是针对实现编程”;
- 这种将多个类结合在一起使用,被叫做 “组合”。在设计时,应该 “多用组合,少用继承”;