策略模式下的思考

工作如何繁忙,生活如何糟心,至少求知的这一刻是我的…

应用场景概述

针对用户不同场景、不同输入采取不同算法或行为时,如果试图在一个方法内实现所有算法或行为,可能使得该方法数百行乃至更多的代码,新增需求便再添加一个分支,洋洋洒洒又是数十行代码甚至更多,维护这个方法会逐渐变得困难。
实际上,用户不关心该场景下的算法、行为有多复杂,对于一个良好的代码,我们要求它具有较强的可阅读性。为了向用户屏蔽复杂性,在OOP中比较直观的方法是将不同的算法、行为封装成一个个对象,这也使得相关的算法或行为得到被复用的机会:

public int doSth(int type) {
    if (type == 0) {
        ActionA aa = new ActionA();
        return aa.exec();
    } else if (type == 1) {
        ActionB ab = new ActionB();
        return ab.exec();
    } else {
        ActionDefault ad = new ActionDefault();
        return ad.exec();
    }
}

上述代码已经暴露了问题,这些对象“各自为政”,没有统一的接口管理它们,协作者在维护代码时,并不会严格遵循我们的想法:一定为这些Action类提供#exec方法。他们可能提供的是#exectue方法、也可能是#doAction方法,这一定程度上破坏了代码的可读性。同时,更为重要的是,这里存在冗余代码,既然都是在调用类似对象的#exec方法,那为何不使用多态来实现呢?

public interface Action {
    int exec();
}

public class ActionA implements Action {
    public int exec() {
        return 0;
    }
}

public class ActionB implements Action {
    public int exec() {
        return 1;
    }
}

public class ActionDefault implements Action {
    public int exec() {
        return 2;
    }
}
public class Sth {
    public int doSth(int type) {
        Action act;
        if (type == 0) {
            act = new ActionA();
        } else if (type == 1) {
            act = new ActionB();
        } else {
            act = new ActionDefault();
        }
        return act.exec();
    }
}

对于doSth方法而言,是用户根据其应用场景创建合适的对象并调用相关方法,上述代码实现可以说是策略模式的雏形,Action接口是抽象策略,各个Action的实现类也就是各个具体策略,它们封装了具体的算法或行为。

策略模式UML

如下图所示,策略模式由三种成员构成:

  • Context: 上下文管理器,负责持有一个策略对象的引用,为客户端提供调用方法,也就是客户端创建所需的策略对象,传入Context,调用Context的方法间接调用具体策略对象的方法;
  • Strategy: 抽象策略接口,定义公共接口,相关的具体策略都应实现它;
  • StrategyA/StrategyB/…: 具体策略实现类,封装具体的算法或行为。
    在这里插入图片描述
    在上一小节中并没有引入Context,事实上,如果只想直接地调用具体策略对象的method方法,并不需要引入Context,使用上一小节末的实现即可。Context的引入,可以在callMethod方法中实现对具体策略对象的功能增强,比如简单的例子:记录策略的执行时间、提供前处理和后处理等等;也可以将各个策略类中获取程序上下文的重复代码交由Context处理。

具体实现

现实中有太多的例子可匹配策略模式,比如PM希望你对各类会员对商品的价格有不同的折扣,需求如下:

  • 普通会员:不打折;
  • 高级会员:95折;
  • 超级会员:9折。

如果我们不使用策略模式实现,我们很容易想到:

public float getPrice(User user, float oriPrice) {
    switch(user.type) {
        case MEMBER.VIP: return oriPrice * 0.95;
        case MEMBER.SUPER_VIP: return oriPrice * 0.9;
        default: return oriPrice;
    }
}

事实上,最早在需求并不复杂的情况,过早的优化反而是未来的噩梦,如此实现就好了。而PM显然不会善罢甘休,他要求不断地细分用户级别,针对不同会员打不同的折,
协作者维护代码时也不会难以理解,无非是扩充MEMBER枚举类和增加case,无需刻意重构。
但人心的贪欲是无尽的,一心一意为资本服务的PM提出:

  • 普通会员如果曾续费过高级会员或超级会员,并且过期时长不超过30天,前台会诱导用户续费会员服务,后台计算价格时根据续费会员等级给该用户提供一张95折或9折优惠券。

此时,switch case在嵌套分支下已难堪大任,我们应当考虑有更细致的价格计算,最简单的就是if分支替换它,但在复杂分支下它的可读性不佳,因此我们不该考虑使用if,尝试使用策略模式实现:

public interface PriceStrategy {
    float getPrice(User user, float oriPrice);
}

public class NormalPriceStrategy implements PriceStrategy {
    public float getPrice(User user, float oriPrice) {
        if (user.superVipExpire < 31) return oriPrice * 0.9;
        else if(user.vipExpire < 31) return oriPrice * 0.95;
        else return oriPrice;
        }
}
public class VipPriceStrategy implements PriceStrategy {
    public float getPrice(User user, float oriPrice) {
        return oriPrice * 0.95;
        }
}

public class SVipPriceStrategy implements PriceStrategy{
    public float getPrice(User user, float oriPrice) {
        return oriPrice * 0.9;
        }
    }

public class PriceContext {
//  采取JavaBean模式,可能引发NullPointerExpetion,但这属于用户编程错误,故无需给ps默认初始化
    private PriceStrategy ps;

    public PriceStrategy getStrategy() {
        return ps;
    }

    public void setStrategy(PriceStrategy ps) {
        this.ps = ps;
    }

    public float getPrice(User user, float oriPrice) {
        return ps.getPrice(user, oriPrice);
    }
}

public class CalPriceService {
    public float calPrice(User user, float oriPrice) {
        PriceContext pc = new PriceContext();
        switch(user.type) {
            case MEMBER.VIP: pc.setStrategy(new VipPriceStrategy()); break;
            case MEMBER.SUPER_VIP: pc.setStrategy(new SVipStrategy()); break;
            default: pc.setStrategy(new NormalStrategy()); break;
        }
        return pc.getPrice(user, oriPrice);
    }
}

可以看到,这次实现的PriceContext并没有附加功能,只是调用了具体策略对象的#getPrice方法,在上文也提过,如果场景比较简单无需对具体策略对象进行功能加强,就不需要实现Context,直接创建相应策略对象并调用相关方法即可。
看到客户端(CalPriceService)的代码时,相信读者对策略模式的缺陷有些明了:编写客户端的协作者必须清楚地知道不同等级会员相应的策略,也就是在什么场景下需要什么策略。那有方法为客户端屏蔽这一层复杂性吗?答案是有的:

  • 抽象策略类应提供#isMatch(objects args…):boolean方法,各个具体策略类实现它,用以判断相关参数是否可匹配该策略;
  • Context中通过反射获取到所有的策略类,并对其遍历传入相关参数即可得到相应的匹配策略。
  • 在客户端调用时,通过Context对象传入相关参数,即可自动实例化相关策略。

策略模式优缺点

优点
  • 避免嵌套分支带来的维护困难、可读性不佳问题;
  • 遵循开闭原则;
  • 相关算法、行为可复用;
  • 将策略与Context分离,在Context中可对具体策略对象进一步功能增强。
缺点
  • 可能会创建大量的策略对象;
  • 客户端协作者必须知道在什么场景应用什么策略。

完美匹配某种模式的场景不多,设计模式只是一种思想并不是标准,无需严格遵守,按照项目的场景实现、改造即可。同时过早的优化,反而会引起后期维护不便,只需要实现当前功能,等到遇到某种瓶颈时再考虑重构、优化的事。

出现这个错误的原因是在导入seaborn包时,无法从typing模块中导入名为'Protocol'的对象。 解决这个问题的方法有以下几种: 1. 检查你的Python版本是否符合seaborn包的要求,如果不符合,尝试更新Python版本。 2. 检查你的环境中是否安装了typing_extensions包,如果没有安装,可以使用以下命令安装:pip install typing_extensions。 3. 如果你使用的是Python 3.8版本以下的版本,你可以尝试使用typing_extensions包来代替typing模块来解决该问题。 4. 检查你的代码是否正确导入了seaborn包,并且没有其他导入错误。 5. 如果以上方法都无法解决问题,可以尝试在你的代码中使用其他的可替代包或者更新seaborn包的版本来解决该问题。 总结: 出现ImportError: cannot import name 'Protocol' from 'typing'错误的原因可能是由于Python版本不兼容、缺少typing_extensions包或者导入错误等原因造成的。可以根据具体情况尝试上述方法来解决该问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [ImportError: cannot import name ‘Literal‘ from ‘typing‘ (D:\Anaconda\envs\tensorflow\lib\typing....](https://blog.youkuaiyun.com/yuhaix/article/details/124528628)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值