Strategy Pattern 教你秒变“神枪手”

156 阅读7分钟

今天介绍的是 Strategy Pattern (策略模式)。同样干货满满,希望看到的你依旧能有所收获~

目的

做一件事情有不同的实现方式, 可以将变化的部分和不变的部分剥离开, 去除大量的 if/else, 提供高扩展性。

例子代码

比如我们想要带妹吃鸡, 就要成为一个神枪手。在各种枪战游戏中, 有各种不同的枪, 我们要根据实际情况(比如,射程)的不同选择不同的枪进行射击。

如果枪的子弹数量都不富裕, 我们要用最少的子弹,最合适的方法达到最强伤害, 最终大吉大利。这样,你想带妹子去哪里吃鸡就去哪里吃鸡啦, 真的是哪里都可以哦。

当我们距离对手:

1米以内,使用平底锅( 想我当时三级头三级甲, 手持 AKM, 满血满状态, 三级包里药包无数, 到了决赛圈被平底锅堵在墙角打死啦😢 );

100 米左右,使用冲锋枪;

超过1000 米,使用狙击枪(对于想我酱紫的小菜鸡,基本流程是开一枪没打中, 暴露位置, 被别人一狙打死...囧)。

/** * 面条式代码判断最强武器 */public class NoodlesKillProcessor {        /**     * 根据距离判断最好的武器击杀对手     * @param distance     */     * 根据距离判断最好的武器击杀对手     * @param distance     */    @BadSmell    public static void killByDistance(int distance) {                if(distance < 0) {                        throw new RuntimeException        ("距离咋还能是负数呢? 我以为只要我能 -20 cm 呢");        }                if(distance >= 0 && distance < 1) {            System.out.println("发现敌人");            System.out.println("二步快速走过去");            System.out.println("掏出平底锅呼死他");                        return;        }                if(distance >= 1 && distance < 10) {            System.out.println("发现敌人");            System.out.println("快速走过去");            System.out.println("掏出手枪打死他");                        return;        }                if(distance >= 10 && distance < 100) {            System.out.println("发现敌人");            System.out.println("身体站直, 心态稳住");            System.out.println("掏出冲锋枪打死他");                        return;        }                if(distance >= 100 && distance < 1000) {            System.out.println("发现敌人");            System.out.println("身体蹲下降低后坐力");            System.out.println("掏出步枪");            System.out.println("打开 3 倍镜");            System.out.println("开枪射击");                        return;        }                if(distance >= 1000) {            System.out.println("发现敌人");            System.out.println("趴在草丛里苟着");            System.out.println("掏出狙击枪");            System.out.println("打开 8 倍镜");            System.out.println("开枪射击");                        return;        }    }}

问题分析

我们觉得这有3个问题,具体分析如下:

01

可读性问题

我看这么多 if/else 语句, 里面的 sout 语句目前三四行也还好, 如果我们有上百行的语句, 里面也有很多 if/else, 这样都不知道下个主 if 跑哪去啦😳

02

重复性问题

全都需要发现敌人, 如果发现敌人是个成百上千行代码, 就很麻烦啦。

03

可维护性问题

如果这时候我们新增了一种枪, 比如是散弹枪, 适用10 到20 的时候使用, 这时候我们就需要在加一个 if 语句如下:

/** * 面条式代码判断最强武器 */public class NoodlesKillProcessor {        /**     * 根据距离判断最好的武器击杀对手     * @param distance     */     * 根据距离判断最好的武器击杀对手     * @param distance     */    @BadSmell    public static void killByDistance(int distance) {                if(distance < 0) {                        throw new RuntimeException            ("距离咋还能是负数呢? 我以为只要我能 -20 cm 呢");        }                if(distance >= 0 && distance < 1) {            System.out.println("发现敌人");            System.out.println("二步快速走过去");            System.out.println("掏出平底锅呼死他");                        return;        }                if(distance >= 1 && distance < 10) {            System.out.println("发现敌人");            System.out.println("快速走过去");            System.out.println("掏出手枪打死他");                        return;        }                if(distance >= 10 && distance < 20) {            System.out.println("发现敌人");            System.out.println("身体站直, 瞄准");            System.out.println("打一枪算一枪");                        return;        }                if(distance >= 20 && distance < 100) {            System.out.println("发现敌人");            System.out.println("身体站直, 心态稳住");            System.out.println("掏出冲锋枪打死他");                        return;        }                if(distance >= 100 && distance < 1000) {            System.out.println("发现敌人");            System.out.println("身体蹲下降低后坐力");            System.out.println("掏出步枪");            System.out.println("打开 3 倍镜");            System.out.println("开枪射击");                        return;        }                if(distance >= 1000) {            System.out.println("发现敌人");            System.out.println("趴在草丛里苟着");            System.out.println("掏出狙击枪");            System.out.println("打开 8 倍镜");            System.out.println("开枪射击");                        return;        }    }}

这个看着也没啥大问题的样子, 不就是加了个 if 么, 但是由于我们改动了这个文件, 测试同学问我们需要测试哪些功能, 说是测一种枪需要 5 天🤦‍♂️

问题来啦, 本来说是你增加一种枪, 需要测 5 天, 但是现在你说改了这文件, 上下可能有些局部变量共享的, 或者有些方法可能改了入参的值, 这些有负作用的方法被调用啦, 所以可能狙击枪也得测一测, 可能手枪也得测一测。

测试同学崩了, 本来 5 天的工作量, 搞成了 5 * 6 天, 一个月都在测枪😫, KPI 都没啦, OKR 都没啦, 钱都没啦, 老婆都跑啦, 自己都绿啦等等等, 不拿刀砍你就是真感情啦😁

初步尝试解决

我们先定义好一个基础类, 解决一下可读性问题和重复性问题。

定义一个基础武器类:

/** * 抽象的枪 */public abstract class Weapon {        /**     * 发现敌人     */     * 发现敌人     */    protected void findEnemy() {        System.out.println("发现敌人");    }        /**     * 开枪前的动作     */     * 开枪前的动作     */    protected abstract void preAction();        /**     * 开枪     */     * 开枪     */    protected abstract void shoot();        /**     * 杀人的动作     */     * 杀人的动作     */    public void kill() {        findEnemy();        preAction();        shoot();    }}

逐个实现武器的具体类, 平底锅, 冲锋枪, 步枪等类如下:

/** * 平底锅 */public class Pan extends Weapon {        @Override    protected void preAction() {        System.out.println("二步快速走过去");    }        @Override    protected void shoot() {        System.out.println("掏出平底锅呼死他");    }}

/** * 手枪类 */public class Pistol extends Weapon {        @Override    protected void preAction() {        System.out.println("快速走过去");    }        @Override    protected void shoot() {        System.out.println("掏出手枪打死他");    }}

/** * 散弹枪 */public class Shotgun extends Weapon {        @Override    protected void preAction() {        System.out.println("身体站直, 瞄准");    }        @Override    protected void shoot() {        System.out.println("打一枪算一枪");    }}

/** * 狙击枪 */public class SniperRifle extends Weapon {        @Override    protected void preAction() {        System.out.println("趴在草丛里苟着");        System.out.println("掏出狙击枪");        System.out.println("打开 8 倍镜");    }    @Override    protected void shoot() {        System.out.println("开枪射击");    }}

/** * 冲锋枪 */public class SubmachineGun extends Weapon {        @Override    protected void preAction() {        System.out.println("身体站直, 心态稳住");    }        @Override    protected void shoot() {        System.out.println("掏出冲锋枪打死他");    }}

我们的方法就可以改动得更清晰啦

/** * 抽象出类代码判断最强武器 */public class WeaponKillProcessor {         * 根据距离判断最好的武器击杀对手     *     * @param distance     */    @BadSmell    public static void killByDistance(int distance) {                if (distance < 0) {                        throw new RuntimeException            ("距离咋还能是负数呢? 我以为只要我能 -20 cm 呢?");        }        Weapon weapon = null;                if (distance >= 0 && distance < 1) {            weapon = new Pan();        } else if (distance >= 1 && distance < 10) {            weapon = new Pistol();        } else if (distance > 10 && distance < 20) {            weapon = new Shotgun();        } else if (distance >= 20 && distance < 100) {            weapon = new SubmachineGun();        } else if (distance >= 100 && distance < 1000) {            weapon = new Rifle();        } else if (distance >= 1000) {            weapon = new SniperRifle();        }        weapon.kill();    }}

类图如下:

使用策略模式

上面的代码没有解决最根本的问题, 也就是去除 if/else, 所用的方法其实就是将 if else 转换为 for, 这样的代码后续添加枪就不需要再增加新的类型啦。

我们先定义一个通用的策略模式接口如下:

/** * 策略模式 */public interface Strategy<T extends AbstractStrategyRequest,R extends AbstractStrategyResponse> {         * 执行策略     * @param request     * @return     */    R executeStrategy(T request);}

入参和出参都是基本的抽象类:

/** * 策略模式抽象入参 */public abstract class AbstractStrategyRequest {}

/** * 策略模式抽象出参 */public abstract class AbstractStrategyResponse {}

实现一个武器抽象类实现接口:

public abstract class WeaponStrategy implements Strategy<WeaponStrategyRequest,AbstractStrategyResponse> {        /**     * 发现敌人     */    protected void findEnemy() {        System.out.println("发现敌人");    }        /**     * 开枪前的动作     */    protected abstract void preAction();        /**     * 开枪     */    protected abstract void shoot();        /**     * 获取距离范围     * @return     */    protected abstract Range<Integer> queryDistanceRange();        /**     * 杀人的动作     */    public void kill() {        findEnemy();        preAction();        shoot();    }        @Override    public AbstractStrategyResponse        executeStrategy(WeaponStrategyRequest request) {        System.out.println("距离敌人 " +                                request.getDistance());        kill();                return null;    }}

其中的 Range 类实现如下:

/** * 范围类 * @param <T> */@Data@AllArgsConstructorpublic class Range<T extends Comparable<T>> {        private T start;        private T end;        public Range(T start, T end) {                this.start = start;                this.end = end;    }        private boolean isIncludeStart = true;        private boolean isIncludeEnd = false;        /**     * 判断是否在范围内     * @param target     * @return     */    public boolean inRange(T target) {                if(isIncludeStart) {                        if(start.compareTo(target) > 0) {                                return false;            }        } else {                        if(start.compareTo(target) >= 0) {                                return false;            }        }                if(isIncludeEnd) {                        if(end.compareTo(target) < 0) {                                return false;            }        } else {                        if(end.compareTo(target) <= 0) {                                return false;            }        }                return true;    }}

依次实现这个抽象武器策略类:

/** * 平底锅 */public class PanStrategy extends WeaponStrategy {    @Override    protected void preAction() {        System.out.println("二步快速走过去");    }    @Override    protected void shoot() {        System.out.println("掏出平底锅呼死他");    }    @Override    protected Range<Integer> queryDistanceRange() {        return new Range<>(0, 1);    }}

/** * 手枪类 */public class PistolStrategy extends WeaponStrategy {        @Override    protected void preAction() {        System.out.println("快速走过去");    }        @Override    protected void shoot() {        System.out.println("掏出手枪打死他");    }        @Override    protected Range<Integer> queryDistanceRange() {                return new Range<>(1, 10);    }}

/** * 步枪 */public class RifleStrategy extends WeaponStrategy {        @Override    protected void preAction() {        System.out.println("身体蹲下降低后坐力");        System.out.println("掏出步枪");        System.out.println("打开 3 倍镜");    }        @Override    protected void shoot() {        System.out.println("开枪射击");    }        @Override    protected Range<Integer> queryDistanceRange() {                return new Range<>(100, 1000);    }}

/** * 散弹枪 */public class ShotgunStrategy extends WeaponStrategy {        @Override    protected void preAction() {        System.out.println("身体站直, 瞄准");    }        @Override    protected void shoot() {        System.out.println("打一枪算一枪");    }        @Override    protected Range<Integer> queryDistanceRange() {                return new Range<>(10, 20);    }}

public class SniperRifleStrategy extends WeaponStrategy {        @Override    protected void preAction() {        System.out.println("趴在草丛里苟着");        System.out.println("掏出狙击枪");        System.out.println("打开 8 倍镜");    }        @Override    protected void shoot() {        System.out.println("开枪射击");    }        @Override    protected Range<Integer> queryDistanceRange() {                return new Range<>(1000, Integer.MAX_VALUE);    }}

/** * 冲锋枪 */public class SubmachineGunStrategy extends WeaponStrategy {        @Override    protected void preAction() {        System.out.println("身体站直, 心态稳住");    }        @Override    protected void shoot() {        System.out.println("掏出冲锋枪打死他");    }        @Override    protected Range<Integer> queryDistanceRange() {                return new Range<>(20, 100);    }}

定义一个上下文类来对入参进行路由:

/** * 策略上下文, 用来路由策略 */public class StrategyContext {    public static final List<WeaponStrategy>        WEAPON_STRATEGYS = new ArrayList<>();    static {        WEAPON_STRATEGYS.add(new PanStrategy());        WEAPON_STRATEGYS.add(new PistolStrategy());        WEAPON_STRATEGYS.add(new RifleStrategy());        WEAPON_STRATEGYS.add(new ShotgunStrategy());        WEAPON_STRATEGYS.add(new SniperRifleStrategy());        WEAPON_STRATEGYS.add(new SubmachineGunStrategy());    }    public static void execute(Integer distance) {        WEAPON_STRATEGYS.stream().        filter((weaponStrategy -> {            Range<Integer> integerRange =                weaponStrategy.queryDistanceRange();            return integerRange.inRange(distance);        })).        findAny().        get().        executeStrategy(            new WeaponStrategyRequest(distance));    }}

最后在主方法里面调用就好啦:

public class App {    public static void main(String[] args) {                int distance = 89;                StrategyContext.execute(89);    }}

结果如下:

距离敌人 89发现敌人身体站直, 心态稳住掏出冲锋枪打死他

类图如下:

其他

这个是比较常见的方法, 基于Spring 的实现和基于反射的实现, 还有生产上的例子会在习题中给出。

策略者模式课后作业

1.了解使用反射和在 spring 框架下策略模式的使用, 将文中的例子转为这二种情况下的代码。

2. 比较利用运行时动态决策策略和初始化的时候就将策略整理到一个集合下的性能差异。

3. 作为一个银行开发人员, 有一天产品经理需要你完成借贷优惠卷计算的部分逻辑, 大致需求如下:

贷款分为本金利息, 输入一种贷款属性 (本金 + 利息) 和优惠卷类型, 需要你计算用户总共应该还的金额从而给用户推荐最省钱的优惠卷

贷款属性类定义如下:

/** * 贷款金额属性 */@Data public class LoanAmtDetail {        /**     * 本金     */    private BigDecimal priceAmt;        /**     * 利息     */    private BigDecimal rateAmt;}

优惠卷类型如下:

一. 利息全免, 本金不变

二. 利息打 8 折, 本金不变

三. 利息固定抵扣抵扣 800 元, 本金不变

四. 本金加利息一起打 9 折

请使用策略模式实现。

这时候产品提出了一个新的优惠卷, 要求本金抵扣 80 元, 利息全免, 我们的改动有多大,测试回归测试的范围有多大🤔

codog代码狗 微信公众号: