《HF 设计模式》 C1 策略模式

文章目录

1 模拟鸭子系统

在一个模拟鸭子游戏的应用中,存在若干种鸭子,有绿头鸭,红头鸭,橡皮鸭,木头鸭等,每一种鸭子都有其独特的行为,该如何设计这个系统,并保证以后有更多的鸭子加入时,系统具有良好的可扩展性和可维护性?

1.1 粗糙的使用继承完成

对于每一种鸭子,我们先思考它们的特点:

重量
高度
...

飞行行为,有的鸭子会飞,有的鸭子不会...
鸣叫行为,有的鸭子呱呱叫,有的鸭子吱吱叫,有的鸭子不会叫...
外貌描述,(绿头,纤细)绿头鸭,(塑料,可爱)模型鸭...
...

不管是绿头鸭,还是红头鸭,模型鸭,其本质都是鸭子,很自然的我们想到了使用继承来解决这个问题:

Duck父类:

/**
 * @author 雫
 * @date 2021/3/1 - 10:17
 * @function 所有鸭子的父类
 * 父类中鸭子的行为方法等待被覆盖
 */
public class Duck {
    protected int weight;
    protected int height;

    public Duck(int weight, int height) {
        this.weight = weight;
        this.height = height;
    }

    public void fly() {}

    public void quack() {}

    public void show() {}

}

MallardDuck绿头鸭子类:

/**
 * @author 雫
 * @date 2021/3/1 - 10:23
 * @function 绿头鸭
 */
public class MallardDuck extends Duck {

    public MallardDuck(int weight, int height) {
        super(weight, height);
    }

    @Override
    public void fly() {
        System.out.println("会飞,飞的很低");
    }

    @Override
    public void quack() {
        System.out.println("呱呱叫");
    }

    @Override
    public void show() {
        System.out.println("绿头,纤细");
    }
}

ModelDuck模型鸭子类:

/**
 * @author 雫
 * @date 2021/3/1 - 10:25
 * @function 模型鸭
 */
public class ModelDuck extends Duck {

    public ModelDuck(int weight, int height) {
        super(weight, height);
    }

    @Override
    public void fly() {
        System.out.println("不会飞");
    }

    @Override
    public void quack() {
        System.out.println("不会叫");
    }

    @Override
    public void show() {
        System.out.println("塑料,可爱");
    }
}

测试:
《HF 设计模式》 C1 策略模式

1.2 使用继承后的问题

使用继承虽然能粗糙的完成模拟鸭子系统,但是随着系统迭代,我们要求给所有鸭子加上游泳,进食,休息等方法

自然的想到去父类Duck中增加新的方法:
《HF 设计模式》 C1 策略模式
但是在父类中增加方法后,我们的ModelDuck可以直接调用swim()方法!

《HF 设计模式》 C1 策略模式
这明显是不合理也不应该发生的,直接的解决方法是在ModelDuck中重写父类的方法,打印不会游泳,或者直接更改swim的权限为private,但是随着系统的升级迭代,当我们需要增加或修改父类Duck中的一个方法时,伴随而来的就是所有子类成千上万次的重写父类方法,这毫无意义

继承虽然能保留“好东西”,但是牵一发而动全身

1.3 软件是不断变化的

不管当初软件设计的多好,一段时间后,总需要成长与改变,否则软件就会变成一堆无意义的碎片

驱动改变的因素有很多:

1,数据库更换
2,客户需要新功能
3,项目需要移植到别的平台
4,和竞品竞争,需要迭代升级
5,为了面对更多的客户,需要重构代码
6,出现了新的技术,可以让原先的代码更好
7,预算有限,无法租用大量高质量服务器,只能在代码上面优化

软件开发完成前和软件开发完成后,“后”需要的精力和时间更多,我们需要花许多时间在系统的维护和变化上,比原来开发所需的时间还要多,所以应该在设计时就致力于提高可维护性和可扩展性

再回到模拟鸭子系统的问题,继承并不能很好地解决问题,因为鸭子的子类在不断的发生变化

除去继承,自然就想到了接口,在使用接口完成问题前,学习一个设计原则

设计原则:
找出应用中可能需要变化之处,把它们独立出来
不要和那些不需要变化的代码放在一起

该设计原则,让系统的某部分改变不会影响其它部分,将使得代码变化引起的不经意后果变少,系统变得更有弹性

1.4 使用接口完成问题

回顾刚才的设计原则,将不变的和改变的分开,再次思考这个模拟鸭子系统,我们再引入一个新的设计原则:

设计原则:
针对接口编程,而不是针对实现编程
不变:
1,重量
2,高度

改变:
1,飞行行为
2,鸣叫行为
3,外貌描述

对于上述改变的行为,采用接口来处理,对于飞行,采用一个统一的飞行接口FlyBehavior,若干种具体飞行方式来实现FlyBehavior接口

FlyBehavior:

/**
 * @author 雫
 * @date 2021/3/1 - 11:19
 * @function 飞行行为接口
 */
public interface FlyBehavior {

    void fly();
}

具体的飞行类:

/**
 * @author 雫
 * @date 2021/3/1 - 11:21
 * @function
 */
public class CantFly implements FlyBehavior {

    @Override
    public void fly() {
        System.out.println("不会飞");
    }
}

/**
 * @author 雫
 * @date 2021/3/1 - 11:21
 * @function
 */
public class FlyHigh implements FlyBehavior {

    @Override
    public void fly() {
        System.out.println("可以飞的很高");
    }
}

我们仍然创建一个父类,这个父类仅包含“不变”的重量和高度,而且能够“动态的”根据子类的特征来进行飞行或叫等行为

Duck父类:

/**
 * @author 雫
 * @date 2021/3/1 - 11:18
 * @function 所有鸭子的父类
 */
public class Duck {
    protected int weight;
    protected int height;

    protected FlyBehavior flyBehavior;
    protected QuackBehavior quackBehavior;

    public Duck(int weight, int height) {
        this.weight = weight;
        this.height = height;
    }

    public void fly() {
        this.flyBehavior.fly();
    }

    public void quack() {
        this.quackBehavior.quack();
    }
}

MallardDuck:

/**
 * @author 雫
 * @date 2021/3/1 - 11:24
 * @function 绿头鸭
 */
public class MallardDuck extends Duck {

    public MallardDuck(int weight, int height) {
        super(weight, height);
        this.flyBehavior = new FlyHigh();
        this.quackBehavior = new Quack();
    }

}

ModelDuck:

/**
 * @author 雫
 * @date 2021/3/1 - 11:33
 * @function 模型鸭
 */
public class ModelDuck extends Duck {

    public ModelDuck(int weight, int height) {
        super(weight, height);
        this.flyBehavior = new CantFly();
        this.quackBehavior = new CantQuack();
    }
}

我们把接口当作成员,在生产具体对象时,通过在构造器中更改这些特殊成员的值,来动态地生成特性不一的鸭子

《HF 设计模式》 C1 策略模式
上述代码中两个对象调用的是父类中的quack()和fly()方法,而父类不是直接执行,而是委托行为类处理

对于这种实现方式,对于FlyBehavior接口,我们可以创建很多实现类来模拟鸭子不同地飞行行为,在生成一种鸭子对象时,在构造器中决定该鸭子的行为特征,这样就可以通过父类中已被调整好的方法来模拟鸭子的行为

这样做,可以让各种鸭子行为与鸭子分离,这些FlyBehavior的实现类也可以完成代码复用,避免了重复的代码,也可以新增/替换一些行为,这些行为的更改通过这种方式尤为简单,这样不仅利用了继承的"复用",还避免了继承的包袱

1.5 使用set方法替换行为

上述的设计虽然让整个系统具有了弹性,但是有一点存在缺陷,行为特征的描述被放在了构造器里,一旦生成该对象,该对象不能再改变
《HF 设计模式》 C1 策略模式
但考虑如下的场景,我要生成两个绿头鸭,绿头鸭A身体健康,绿头鸭B存在残缺,绿头鸭A可以飞的很高且可以叫,而绿头鸭B不能飞,只能叫

但是之前的代码绿头鸭类已经写好,默认构造生成了能飞能叫的绿头鸭,这里不可能为B生成一个类并写好它的默认构造,这就是同类对象替换行为的需求,自然想到的set方法

我们回到Duck,为两个接口类型的成员添加set方法:

/**
 * @author 雫
 * @date 2021/3/1 - 11:18
 * @function 所有鸭子的父类
 */
public class Duck {
    protected int weight;
    protected int height;

    public void setFlyBehavior(FlyBehavior flyBehavior) {
        this.flyBehavior = flyBehavior;
    }

    public void setQuackBehavior(QuackBehavior quackBehavior) {
        this.quackBehavior = quackBehavior;
    }

    protected FlyBehavior flyBehavior;
    protected QuackBehavior quackBehavior;

    public Duck(int weight, int height) {
        this.weight = weight;
        this.height = height;
    }

    public void fly() {
        this.flyBehavior.fly();
    }

    public void quack() {
        this.quackBehavior.quack();
    }
}

有了set方法,就可以生成鸭子对象后替换它的行为:
《HF 设计模式》 C1 策略模式

1.6 多用组合,少用继承

《HF 设计模式》 C1 策略模式
对于每个行为,我们为其准备一系列的实现类,这些实现类称为一族算法,这些“算法”被用于构建对象,为对象赋予特征或替换对象的同类特征

在模拟鸭子的系统中,采用了组合的模式,每一个鸭子都有FlyBehavior和QuackBehavior,将这两个类结合起来使用,这就是组合

这里再次引入一个设计原则:

设计原则:
多用组合,少用继承

使用组合建立的系统具有很大的弹性,不仅可以将算法族封装成类,更可以在生成对象后改变其特征

1.7 策略模式

策略模式:定义算法族,分别封装起来,让它们之间可以相互替换,此模式让算法的变化独立于使用算法的客户

即为某个可能变化的功能,设计一个接口,为该接口扩展出一系列实现类,在生成该对象时再赋予该对象特有的特征,让功能的实现与对象脱离,并可以替换该特征,将对象和行为分离,以降低耦合,提高代码复用

1.8 关于设计模式

我们在开发的过程中大量使用Java API,但库和框架无法帮助我们将应用组织成容易了解,容易维护,具有弹性的结构,为此需要设计模式,当学习过设计模式后,就应该在新设计中采用它们,或者利用设计模式重构混乱的旧代码

知道抽象,继承,多态并不意味了解了设计模式,设计模式关心的是建立弹性的设计,可以维护,可以应付变化,良好的设计必须满足:可复用,可扩充,可维护

上一篇:个人主页展示系统(springBoot)-no6-信息查询功能开发


下一篇:CF习题集二