Rust语言之GoF设计模式:责任链模式

22-09-26 banq

责任链是一种行为设计模式,它允许沿着潜在处理程序链传递请求,直到其中一个处理请求。

责任链也称为职责链,功能链或过滤器模式,当有很多过滤器,无法依附于原有被过滤的对象,可以独立出来成为独立通用的一个大的过滤器集合时,就从decorator模式转为责任链模式了。

先看看stackoverflow上一个错误的责任链实现

pub trait Policeman<'a> {
    fn set_next(&'a mut self, next: &'a Policeman<'a>);
}

pub struct Officer<'a> {
    deduction: u8,
    next: Option<&'a Policeman<'a>>,
}

impl<'a> Officer<'a> {
    pub fn new(deduction: u8) -> Officer<'a> {
        Officer {deduction, next: None}
    }
}

impl<'a> Policeman<'a> for Officer<'a> {
    fn set_next(&'a mut self, next: &'a Policeman<'a>) {
        self.next = Some(next);
    }
}

fn main() {
    let vincent = Officer::new(8);    // -+ vincent 进入生命周期范围scope
    let mut john = Officer::new(5);   // -+ john 进入生命周期范围scope
    let mut martin = Officer::new(3); // -+ martin 进入生命周期范围scope
                                      //  |
    john.set_next(&vincent);          //  |
    martin.set_next(&john);           //  |
}                                


这会产生错误消息:

error[E0597]: `john` does not live long enough
  --> src\main.rs:29:1
   |
27 |     john.set_next(&vincent);
   |     ---- borrow occurs here
28 |     martin.set_next(&john);
29 | }
   | ^ `john` dropped here while still borrowed
   |
   = note: values in a scope are dropped in the opposite order they are created

error[E0597]: `martin` does not live long enough
  --> src\main.rs:29:1
   |
28 |     martin.set_next(&john);
   |     ------ borrow occurs here
29 | }
   | ^ `martin` dropped here while still borrowed
   |
   = note: values in a scope are dropped in the opposite order they are created

error[E0597]: `john` does not live long enough
  --> src\main.rs:29:1
   |
28 |     martin.set_next(&john);
   |                      ---- borrow occurs here
29 | }
   | ^ `john` dropped here while still borrowed
   |
   = note: values in a scope are dropped in the opposite order they are created


疑问:
为什么john活得不够长?
  1. 已创建vincent
  2. 已创建john
  3. 已创建martin
  4. john引用vincent(vincent在scope范围内)
  5. martin引用john (john在scope范围内)
  6. martin超出范围(john仍在scope范围内)
  7. john超出范围(vincent仍在scope范围内)
  8. vincent超出范围

这里问题是:
如果您在Rust中多行多个位置使用相同的生命周期参数,编译器必须找到同时满足所有三个位置的具体生命周期。它会混乱。
在编译:

    john.set_next(&vincent);  

时,编译器会在vincent和 john之间混乱。。。

解决办法
使用泛型将实现范围缩小到严格的编译时类型似乎相当困难:为了构造一个完整链的类型,Rust 需要完全了解链中的“下一个”链接。因此,它看起来像这样:

let mut reception = Reception::<Doctor::<Medical::<Cashier>>>::new(doctor); 


只有使用Box,Box允许以任何组合进行链接:

reception.rs

use super::{into_next, Department, Patient};

#[derive(Default)]
pub struct Reception {
    next: Option<Box<dyn Department>>,
}

impl Reception {
    pub fn new(next: impl Department + 'static) -> Self {
        Self {
            next: into_next(next),
        }
    }
}

impl Department for Reception {
    fn handle(&mut self, patient: &mut Patient) {
        if patient.registration_done {
            println!("Patient registration is already done");
        } else {
            println!("Reception registering a patient {}", patient.name);
            patient.registration_done = true;
        }
    }

    fn next(&mut self) -> &mut Option<Box<dyn Department>> {
        &mut self.next
    }
}


Department是职责链的统一接口:

department.rs

mod cashier;
mod doctor;
mod medical;
mod reception;

pub use cashier::Cashier;
pub use doctor::Doctor;
pub use medical::Medical;
pub use reception::Reception;

use crate::patient::Patient;

/// 组成一条链的统一角色。
/// 一个典型的trait接口实现必须有`handle`和`next`方法。
/// 而` execute'是默认实现,并包含一个适当的链式
////逻辑。
pub trait Department {
    fn execute(&mut self, patient: &mut Patient) {
        self.handle(patient);

        if let Some(next) = &mut self.next() {
            next.execute(patient);
        }
    }

    fn handle(&mut self, patient: &mut Patient);
    fn next(&mut self) -> &mut Option<Box<dyn Department>>;
}

/// Helps to wrap an object into a boxed type.
pub(self) fn into_next(
    department: impl Department + Sized + 'static,
) -> Option<Box<dyn Department>> {
    Some(Box::new(department))
}


这个Department接口trait下面三个真正职责链实现:
  • doctor.rs

    use super::{into_next, Department, Patient};
    
    pub struct Doctor {
        next: Option<Box<dyn Department>>,
    }
    
    impl Doctor {
        pub fn new(next: impl Department + 'static) -> Self {
            Self {
                next: into_next(next),
            }
        }
    }
    
    impl Department for Doctor {
        fn handle(&mut self, patient: &mut Patient) {
            if patient.doctor_check_up_done {
                println!("A doctor checkup is already done");
            } else {
                println!("Doctor checking a patient {}", patient.name);
                patient.doctor_check_up_done = true;
            }
        }
    
        fn next(&mut self) -> &mut Option<Box<dyn Department>> {
            &mut self.next
        }
    }
    
  • medical.rs
  • cashier.rs


reception.rs中使用Box<dyn Department>>原因:

Rust编译器需要知道每个函数的返回类型需要多少空间。这意味着你的所有函数都必须返回一个具体的类型。与其他语言不同的是,如果你有一个像Department这样的接口trait,你不能编写一个直接返回Department的函数,因为Department的不同实现(如doctor、medical、cashier等)需要不同的内存。

然而,有一个简单的变通方法。函数不用直接返回一个trait特质对象Department,而是返回一个包含一些Department的Box。
这个Box只是对堆中一些内存的引用,因为一个引用有一个静态已知的大小,而且编译器可以保证它指向一个堆中的Department。

Rust在分配堆上的内存时,会尽可能地明确。
因此,如果你的函数以这种方式返回一个指向堆上trait的指针,你需要用dyn关键字来写返回类型,例如Box<dyn Department>。




客户端调用代码:

mod department;
mod patient;

use department::{Cashier, Department, Doctor, Medical, Reception};
use patient::Patient;

fn main() {
    let cashier = Cashier::default();
    let medical = Medical::new(cashier);
    let doctor = Doctor::new(medical);
    let mut reception = Reception::new(doctor);

    let mut patient = Patient {
        name: "John".into(),
        ..Patient::default()
    };

    // 接待处处理一个病人,把他交给链条中的下一个环节。
    // 接待处 -> 医生 -> 医疗 -> 收银员。
    // Reception -> Doctor -> Medical -> Cashier.
    reception.execute(&mut patient);

    println!("\nThe patient has been already handled:\n");

    reception.execute(&mut patient);
}