Rust·设计模式·行为变化模式

 

Command模式:把请求封装成对象,可以方便压栈实现redo/undo操作 Visitor模式: 不更改Element类层次结构的前提下 在运行时透明地为类层次结构上的各个类动态添加新的操作(支持变化)

  • Command模式:把请求封装成对象,可以方便压栈实现redo/undo操作
  • Visitor模式:
    • 不更改Element类层次结构的前提下
    • 在运行时透明地为类层次结构上的各个类动态添加新的操作(支持变化)

行为变化模式

  • 在组件的构建过程中
    • 组件行为的变化会导致组件本身剧烈的变化
  • 行为变化模式把组件的行为和组件本身解耦
    • 从而支持组件行为的变化
    • 实现两者之间的松耦合
  • 经典模式
    • Command
    • Visitor

Command模式

动机

  • 在软件构建过程中,行为请求者和行为实现者通常是紧耦合
    • 但在某些场合这种无法抵御变化的紧耦合是不合适的
      • 比如需要为行为进行记录、撤销、重做等
  • 如何将行为请求者与行为实现者解耦?
    • 将一组行为抽象为对象
    • 可以实现二者之间的松耦合

模式定义

  • 将一个请求(行为)封装为一个对象,从而使你可用不同的请求对客户进行参数化
  • 对请求排队或记录请求日志,以及支持可撤销的操作

结构

模式实现

  • 定义Command基类

    class Command {
    public:
      virtual void execute() = 0;
    }
    
  • 定义具体的类

    class ConcreteCommand1: public Command {
      string args;
    public:
      ConcreteCommand1(const string &a): args(a) {}
      void execute() override {
        cout << "#1 process..." << args << endl;
      };
    }
    
    class ConcreteCommand2: public Command {
      string args;
    public:
      ConcreteCommand1(const string &a): args(a) {}
      void execute() override {
        cout << "#2 process..." << args << endl;
      };
    }
    
  • 定义命令组

    class MacroCommand: public Command {
      vector<Command *> commands;
    public:
      void addCommand(Command *c) {commands.push_back(c);}
      void execute() override {
        for (auto &c: commands) {
          c->execute();
        }
      }
    }
    
  • 使用

    int main() {
      ConcreteCommand1 command1(receiver, "Arg ###");
      ConcreteCommand2 command2(receiver, "Arg $$$");
    
      MacroCommand macro;
      macro.addCommand(&command1);
      macro.addCommand(&command2);
    
      macro.execute();
    }
    
  • 核心
    • 我们可用的command1,command2,macro都是对象
    • 但它们表达的都是行为
    • 而对象可以当参数传递、字段来存储、可以序列化、可以存在某些数据结构中
  • 编辑器中的命令
    • 粘贴、剪切、复制都可以是命令对象
    • 这些命令可以存储在栈中
    • 实现redo/undo操作

要点总结

  • Command模式目的在于
    • 将行为请求者与行为实现者解耦
    • 实现手段是将行为抽象为对象
  • 实现Command接口的具体命令对象ConcreteCommand有时根据需要可能会保存一些额外的状态信息
    • 通过使用Composite模式,可以将多个命令封装为一个复合命令MacroCommand
  • Command模式与cpp中的函数对象有些类似
    • Command以面向对象中的接口-实现来定义行为接口规范,有性能损失
    • cpp函数对象以函数签名来定义行为接口规范

Rust 实现

  • 命令模式有几个角色
    • 命令接口
      • 指定执行的命令
    • 命令接收者
      • 也是执行命令的对象,或者是命令作用的对象
    • 具体的命令对象
      • 把命令作用的对象和命令的动作结合起来的对象
    • 命令调用者
      • 把命令对象集中到列表中
      • 到时候集中一个个执行命令

结构

实现的功能

  • 证券交易所
    • 我(client)
      • 创建买入、卖出股票订单(command)
    • 代理(invoker)
      • 股票交易APP
      • 负责记录提交的订单(ConcreteCommand)
    • 证券交易所(receiver)
      • 命令接收者
      • 是最终执行命令的主体

结构

具体实现

trait 订单 {
    fn 执行(&self);
}

struct 买入股票 {
    数量: u32,
    股票: String,
}
impl 买入股票 {
    fn new(股票: String, 数量: u32) -> Self {
        Self { 股票, 数量 }
    }
}
impl 订单 for 买入股票 {
    fn 执行(&self) {
        println!("买入 {} {} 手股票", self.股票, self.数量);
    }
}

struct 卖出股票 {
    数量: u32,
    股票: String,
}
impl 卖出股票 {
    fn new(股票: String, 数量: u32) -> Self {
        Self { 股票, 数量 }
    }
}
impl 订单 for 卖出股票 {
    fn 执行(&self) {
        println!("卖出 {} {} 手股票", self.股票, self.数量);
    }
}

struct 交易App {
    命令列表: Vec<Box<dyn 订单>>,
}
impl 交易App {
    fn new() -> Self {
        Self {
            命令列表: vec![]
        }
    }
    fn 下单(&mut self, 新订单: Box<dyn 订单>) {
        self.命令列表.push(新订单);
    }
    fn 执行命令(&self) {
        println!("\n===== 交易 APP 执行订单 ======");
        for 命令 in self.命令列表.iter() {
            命令.执行();
        }
    }
}

fn main() {
    let mut 交易_app = 交易App::new();
    交易_app.下单(Box::new(买入股票::new("启明信息".to_string(), 800)));
    交易_app.下单(Box::new(卖出股票::new("中国电影".to_string(), 1200)));
    交易_app.下单(Box::new(买入股票::new("中国电影".to_string(), 1200)));
    交易_app.下单(Box::new(卖出股票::new("启明信息".to_string(), 700)));
    交易_app.下单(Box::new(买入股票::new("国金证券".to_string(), 700)));
    交易_app.下单(Box::new(卖出股票::new("国金证券".to_string(), 300)));
    交易_app.下单(Box::new(买入股票::new("维远股份".to_string(), 1000)));
    交易_app.下单(Box::new(卖出股票::new("维远股份".to_string(), 1000)));

    交易_app.执行命令();
}

执行结果:

===== 交易 APP 执行订单 ======
买入 启明信息 800 手股票
卖出 中国电影 1200 手股票
买入 中国电影 1200 手股票
卖出 启明信息 700 手股票
买入 国金证券 700 手股票
卖出 国金证券 300 手股票
买入 维远股份 1000 手股票
卖出 维远股份 1000 手股票

访问器

动机

  • 由于需求改变,某些类的层次结构中常常需要增加新的行为(方法)
    • 如果直接在基类修改,为给子类带来繁重的负担
    • 甚至破坏原有设计
  • 如何在不修改类层次结构的前提下
    • 在运行时根据需要透明地为类层次结构上的各类动态添加新的操作
    • 以避免上面的问题

模式定义

表示一个作用于某对象结构中的各元素的操作。使得可以在不改变(稳定)各元素的类的前提下定义(扩展)作用于这些元素的新操作(变化)

结构

初始设计

  • Element类

    class Element {
    public:
      virtual void Func1() = 0;
    }
    
  • 子类

    class ElementA {
    public:
      void Func1() override {
        // ....
      }
    }
    
    class ElementB {
    public:
      void Func1() override {
        // ....
      }
    }
    
  • 当我们已经完成了软件设计,并已经部署了之后

    • 基类Element要增加一个新的方法Func2时,会影响后面的子类

重构设计

  • 首先我们预计Element这个基类未来会增加新的操作(前提)
    • 但不知道会加多少,会加什么操作
  • 预先设计

    class Element {
    public:
      // 第一次多态辨析
      virtual void accept(Visitor &visitor) = 0;
    
      virtual ~Element(){};
    }
    
  • Visitor类

    class Visitor {
    public:
      virtual void visitElementA(Element& element) = 0;
      virtual void visitElementB(Element& element) = 0;
      virtual ~Visitor(){};
    }
    
  • 子类

    class ElementA: public Element {
    public:
      void accept(Visitor &visitor) override {
        visitor.visitElementA(*this);
      }
    }
    
    class ElementB: public Element {
    public:
      void accept(Visitor &visitor) override {
        // 第二次多态辨析
        visitor.visitElementB(*this);
      }
    }
    
  • 现在要给原来的类层次结构增加新的操作

    // 扩展1
    class Visitor1: public Visitor {
    public:
      void visitElementA(ElementA &element) override {
        cout << "Visitor1 is processing ElementA" <<endl;
      }
    
      void visitElementB(ElementB &element) override {
        cout << "Visitor1 is processing ElementB" <<endl;
      }
    }
    
    // 扩展2
    class Visitor2: public Visitor {
    public:
      void visitElementA(ElementA &element) override {
        cout << "Visitor2 is processing ElementA" <<endl;
      }
    
      void visitElementB(ElementB &element) override {
        cout << "Visitor2 is processing ElementB" <<endl;
      }
    }
    
  • 使用

    int main() {
      Visitor2 visitor;
      ElementB element;
      element.accept(visitor); // double dispatch
    
      ElementA elementA;
      elementA.accept(visitor);// 双重分发
    }
    
    • 给ElementB添加Visitor2的操作
  • 缺点
    • 要想Visitor稳定,Element的子类个数必须确定
    • 因为Element增加一个子类,Visitor基类就必须修改
    • Element有10个子类,Visitor就要有10个访问方法

要点总结

  • Visitor模式使用双重分发实现在不更改(不添加新操作-编译时)Element类层次结构的前提下
    • 在运行时透明地为类层次结构上的各个类动态添加新的操作(支持变化)
  • 双重分发
    • Visitor模式中间包括了两个多态分发
    • 第1个为accept方法的多态辨析
    • 第2个为visitElemntx方法的多态辨析
  • 最大缺点
    • 扩展类层次结构(增加新的Element子类),会导致Visitor类的改变
    • 因此Visitor模式适用于Element类层次结构稳定
      • 子类的个数是确定的
    • 而其中的操作却经常面临频繁改动
      • 想给子类加各种新操作