Rust·设计模式·Strategy 策略模式

 

如何应对多if..else..的情况 更好的理解什么是封闭开放原则 优雅的实现扩展

  • 如何应对多if..else..的情况
  • 更好的理解什么是封闭开放原则
  • 优雅的实现扩展

缘起·动机(Motivation)

  • 某些对象使用的算法
    • 多种多样,经常改变
    • 问题
      • 将这些算法都编码到对象中,将会使对象变得异常复杂
      • 支持不使用的算法也是一个性能负担
        • 程序最后放在CPU高级缓冲区
        • 程序过大,会放到内存甚至硬盘
        • 所以一个类中包含用不到的分支也会影响性能
          • 税率类如果在中国使用,其他国家的税率算法就不要包含在类中
  • 解决思路
    • 运行时根据需要透明地更改对象的算法
    • 将算法与对象本身解耦

代码演进

  • 项目
    • 跨国税率的计算
    • 包括中、美、德等国的税率

      初始设计·分而治之

  • 由枚举类型定义不同的国家

    enum TaxBase {
      CN_Tax,
      US_Tax,
      DE_Tax,
      FR_Tax       //更改
    };
    
  • 在销售订单中根据不同国家实现一个税率计算的方法

    class SalesOrder{
        TaxBase tax;
    public:
        double CalculateTax(){
    
          if (tax == CN_Tax){ //CN*********** }
          else if (tax == US_Tax){ //US*********** }
          else if (tax == DE_Tax){ //DE*********** }
          // 新增国家就涉及到修改这个类
          else if (tax == FR_Tax){  //...  }
        }
    };
    
  • 分析设计
    • 考虑问题未来的情况
      • 可能支持更多的国家
        • [修改] 枚举类型
        • [修改] 销售订单类
    • 违背了【对扩展开放、对修改封闭】的原则
      • 也就是我这个SalersOrder不能修改
      • 但对新增的功能(文件)又可以支持

设计演进·统而治之

  • 之前有枚举记录的国家,其实是把它们看做一个个不同的个体,分开应对
  • 如果把分开的个体统一成一个抽象的整体,设计方案就更上一层了
  • 不用枚举类型记录具体的国家,而增加一个抽象的基类

    class TaxStrategy{
    public:
        virtual double Calculate(const Context& context)=0;
        virtual ~TaxStrategy(){}
    };
    
  • 所有不同的国家都继承(类属/统一于)这个基类

    class CNTax : public TaxStrategy{
    public:
        virtual double Calculate(const Context& context){
            //***********
        }
    };
    
    class USTax : public TaxStrategy{
    public:
        virtual double Calculate(const Context& context){
            //***********
        }
    };
    
    class DETax : public TaxStrategy{
    public:
        virtual double Calculate(const Context& context){
            //***********
        }
    };
    

    工程开发时,这些类都应该在不同的文件中保存

    • 原来通过枚举分开的计算税率的算法分支在这里都分到的不同子类的Calculate方法中了
  • 客户端SalesOrder
    • 原来不同国家的枚举变量 -> 抽象类 TaxStrategy (多态)指针
    • 通过工厂方法返回不同的国家税率算法
    • 在计算税率的方法中,通过多态调用,调用真正国家的税率算法
    class SalesOrder{
    private:
        TaxStrategy* strategy;
    
    public:
        SalesOrder(StrategyFactory* strategyFactory){
            this->strategy = strategyFactory->NewStrategy();
        }
        ~SalesOrder(){
            delete this->strategy;
        }
    
        public double CalculateTax(){
            // 构建执行环境所需的变量
            Context context();
              
            double val = 
                strategy->Calculate(context); //多态调用
            //...
        }
    };
    
  • 如果新增法国(扩展)
    • [增加] 法国的策略类

      class FRTax : public TaxStrategy {...}
      
    • [修改] 工厂方法,可以返回法国策略
    • 其他地方都不用动
  • 分而治之
    • [修改] 枚举类型
    • [修改] 类,增加法国的分支

复用:编译单位,在二进制层面的复用;不是源代码层面的复用

模式定义

定义一系列算法,把它们一个个封装起来,并且使它们可互相替换(变化)。该模式使得算法可独立于使用它的客户程序(稳定)而变化(扩展,子类化)。

模式类图

要点总结

  • Strategy及其子类为组件提供了一系列可重用的算法
    • 可以使得类型在运行时方便地根据需要在各个算法之间进行切换
      • 运行时传入一个多态的对象
      • 再计算税率时用多态的调用
  • Strategy模式提供了用条件判断语句以外的另一种选择
    • 消除条件判断语句,就是在解耦合
    • 含有许多条件判断语句的代码通常都需要Strategy模式
    • if ... else .../ switch..case.. 是结构化设计中分而治之的思维
    • 只有在if..else..是绝对不变(不会扩展的可能)的情况下才不用重构
      • 由性别产生的不同分支
      • 因为性别是相对稳定的
      • 一周7天这种不同
  • 如果Strategy对象没有实例变量
    • 那么各个上下文可以共享同一个Strategy对象,从而节省对象开销

Rust 实现

  • 商场收银软件
    • 从文本框输入单价和数量
    • 从下拉菜单选择
      • 正常收费
      • 打八折
      • 打七折
      • 打五折
      • 满300送100
      • 满200送50
    • 通过计算后返回当前价格

初始设计

use rand::Rng;

extern crate rand;

fn 下拉菜单选项() -> &'static str {
    let 收款方式选项 = vec![
        "正常收款",
        "打八折",
        "打七折",
        "打五折",
        "满100送50",
        "满200送100",
    ];
    let mut rand_num = rand::thread_rng();
    let idx = rand_num.gen_range(0, 5);
    println!("收款方式: {}", 收款方式选项[idx]);
    return 收款方式选项[idx];
}

fn main() {
    let 收款方式选项: &str = 下拉菜单选项();
    let price = 100.0;
    let num = 20.0;
    let total = price * num;
    println!("总价:${}", total);

    let total = match 收款方式选项 {
        "正常收款" => total,
        "打八折" => 0.8 * total,
        "打七折" => 0.7 * total,
        "打五折" => 0.5 * total,
        "满100送50" => total - (total as u64 / 100) as f64 * 50.0,
        "满200送100" => total - (total as u64 / 200) as f64 * 100.0,
        _ => price * num,
    };

    println!("优惠价格: ${}", total);
}

  • 问题
    • 可扩展:可随意增加新的收款方式

重构设计·策略模式

use rand::Rng;

extern crate rand;

fn 获取优惠方式() -> &'static str {
    let mut rng = rand::thread_rng();
    let s = vec![
        "正常收费", "打八折", "打七折", "打五折", "满100返50", "满200返100",
         ];
    let r = rng.gen_range(0, 6);
    return s[r];
}

struct 优惠场景 {
    优惠算法: Box<dyn 优惠>,
}
impl 优惠场景 {
    fn new(优惠算法: Box<dyn 优惠>) -> Self {
        Self { 优惠算法 }
    }
    fn 得到结果(&self, 原价: f64) -> f64 {
        self.优惠算法.计算(原价)
    }
}

trait 优惠 {
    fn 计算(&self, 原价: f64) -> f64;
}
struct 正常收费;
impl 优惠 for 正常收费 {
    fn 计算(&self, 原价: f64) -> f64 {
        原价
    }
}

struct 打折 {
    折扣: f64,
}
impl 打折 {
    fn new(折扣: f64) -> Self {
        Self { 折扣 }
    }
}
impl 优惠 for 打折 {
    fn 计算(&self, 原价: f64) -> f64 {
        self.折扣 * 原价
    }
}
struct 满减 {
    : f64,
    : f64,
}
impl 满减 {
    fn new(: f64, : f64) -> Self {
        Self { ,  }
    }
}
impl 优惠 for 满减 {
    fn 计算(&self, 原价: f64) -> f64 {
        原价 - (原价 / self.满).floor() * self.减
    }
}

fn main() {
    let mut rng = rand::thread_rng();
    let 原价 = rng.gen_range(0, 2000) as f64;

    let 优惠方式 = 获取优惠方式();

    let 当前优惠场景 = match 优惠方式 {
        "正常优惠" => 优惠场景::new(Box::new(正常收费)),
        "打八折" => 优惠场景::new(Box::new(打折::new(0.8))),
        "打七折" => 优惠场景::new(Box::new(打折::new(0.7))),
        "打五折" => 优惠场景::new(Box::new(打折::new(0.5))),
        "满100返50" => 优惠场景::new(Box::new(满减::new(100.0, 50.0))),
        "满200返100" => 优惠场景::new(Box::new(满减::new(200.0, 100.0))),
        _ => 优惠场景::new(Box::new(正常收费)),
    };
    let 优惠价 = 当前优惠场景.得到结果(原价);

    println!("优惠方式: {}", 优惠方式);
    println!("原价: {}, 优惠价: {:.2}", 原价, 优惠价);
}

简单工厂 + 策略模式

  • 简单工厂放到优惠场景中,其他类不变
  • main的应用与算法的耦合更低
struct 优惠场景 {
    优惠算法: Box<dyn 优惠>,
}
impl 优惠场景 {
    fn new(优惠选项: &str) -> Self {
        let 优惠算法: Box<dyn 优惠> = match 优惠选项 {
            "正常优惠" => Box::new(正常收费),
            "打八折" => Box::new(打折::new(0.8)),
            "打七折" => Box::new(打折::new(0.7)),
            "打五折" => Box::new(打折::new(0.5)),
            "满100返50" => Box::new(满减::new(100.0, 50.0)),
            "满200返100" => Box::new(满减::new(200.0, 100.0)),
            _ => Box::new(正常收费),
        };
        Self { 优惠算法 }
    }
    fn 得到结果(&self, 原价: f64) -> f64 {
        self.优惠算法.计算(原价)
    }
}       


fn main() {
    let mut rng = rand::thread_rng();
    let 原价 = rng.gen_range(0, 2000) as f64;

    let 优惠方式 = 获取优惠方式();

    let 当前优惠场景 = 优惠场景::new(优惠方式);
    let 优惠价 = 当前优惠场景.得到结果(原价);

    println!("优惠方式: {}", 优惠方式);
    println!("原价: {}, 优惠价: {:.2}", 原价, 优惠价);
}

工厂方法给出的是不同的类实例,而策略模式针对着相同的算法接口给出不同的算法实例。一个给的是不同的类,一个给出的是相同的接口(函数调用方式)不同的内容