Rust語言之GoF設計模式:責任鏈模式

banq 發表於 2022-09-26
設計模式 Go

責任鏈是一種行為設計模式,它允許沿著潛在處理程式鏈傳遞請求,直到其中一個處理請求。

責任鏈也稱為職責鏈,功能鏈或過濾器模式,當有很多過濾器,無法依附於原有被過濾的物件,可以獨立出來成為獨立通用的一個大的過濾器集合時,就從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);
}