notes

Log | Files | Refs | README

chain_of_responsibility.md (5940B)


      1 # Chain of Responsibility
      2 
      3 The Chain of Responsibility (CoR) pattern is a behavioral design pattern where a
      4 request is passed along a chain of handlers — each handler either processes the
      5 request or forwards it to the next one in the chain.
      6 
      7 The three key components are: ​
      8 
      9 - Handler — a trait defining the interface for handling requests and holding the
     10   successor link
     11 
     12 - Successor — the next handler in the chain, stored as Option<Box<dyn Handler>>
     13 
     14 - Request — the data passed along the chain
     15 
     16 > The Rust community's guiding principle here is: static where you can, dynamic
     17 > where you must.
     18 
     19 ### Example 1: Purchase Approval Chain (Dynamic Disptach)
     20 
     21 The key pattern here is the Option<Box<dyn Approver>> field — it lets each
     22 handler optionally own its successor and forward the request via
     23 next.process_request(request).
     24 
     25 ```rust
     26 struct PurchaseRequest {
     27     amount: f64,
     28 }
     29 
     30 trait Approver {
     31     fn set_successor(&mut self, successor: Box<dyn Approver>);
     32     fn process_request(&self, request: &PurchaseRequest);
     33 }
     34 
     35 struct Manager {
     36     successor: Option<Box<dyn Approver>>,
     37 }
     38 
     39 impl Approver for Manager {
     40     fn set_successor(&mut self, successor: Box<dyn Approver>) {
     41         self.successor = Some(successor);
     42     }
     43     fn process_request(&self, request: &PurchaseRequest) {
     44         if request.amount <= 1000.0 {
     45             println!("Manager approves ${}", request.amount);
     46         } else if let Some(ref next) = self.successor {
     47             next.process_request(request); // pass it up
     48         } else {
     49             println!("Cannot be approved.");
     50         }
     51     }
     52 }
     53 
     54 struct Director { successor: Option<Box<dyn Approver>> }
     55 
     56 impl Approver for Director {
     57     fn set_successor(&mut self, successor: Box<dyn Approver>) {
     58         self.successor = Some(successor);
     59     }
     60     fn process_request(&self, request: &PurchaseRequest) {
     61         if request.amount <= 5000.0 {
     62             println!("Director approves ${}", request.amount);
     63         } else if let Some(ref next) = self.successor {
     64             next.process_request(request);
     65         } else {
     66             println!("Cannot be approved.");
     67         }
     68     }
     69 }
     70 
     71 struct President;
     72 
     73 impl Approver for President {
     74     fn set_successor(&mut self, _: Box<dyn Approver>) {} // terminal node
     75     fn process_request(&self, request: &PurchaseRequest) {
     76         if request.amount <= 10000.0 {
     77             println!("President approves ${}", request.amount);
     78         } else {
     79             println!("Request denied.");
     80         }
     81     }
     82 }
     83 
     84 fn main() {
     85     let president = President;
     86     let mut director = Director { successor: Some(Box::new(president)) };
     87     let mut manager = Manager { successor: Some(Box::new(director)) };
     88 
     89     manager.process_request(&PurchaseRequest { amount: 500.0 });   // Manager approves
     90     manager.process_request(&PurchaseRequest { amount: 3000.0 });  // Director approves
     91     manager.process_request(&PurchaseRequest { amount: 8000.0 });  // President approves
     92     manager.process_request(&PurchaseRequest { amount: 15000.0 }); // Denied
     93 }
     94 ```
     95 
     96 ### Example 2: Purchase Approval Chain (Generics - Zero-cost static dispatch)
     97 
     98 This is verbose but gives you zero-cost static dispatch — no heap allocation, no
     99 vtable lookup, all method calls resolved at compile time.
    100 
    101 `Handler<Manager<Director<President>>>`
    102 
    103 ```rust
    104 trait Handler {
    105     fn handle(&self, amount: f64);
    106 }
    107 
    108 // Terminal handler — no successor
    109 struct President;
    110 
    111 impl Handler for President {
    112     fn handle(&self, amount: f64) {
    113         if amount <= 10_000.0 {
    114             println!("President approves ${amount}");
    115         } else {
    116             println!("Request denied.");
    117         }
    118     }
    119 }
    120 
    121 // Generic handler wrapping the next handler N
    122 struct Manager<N: Handler> {
    123     next: N,
    124 }
    125 
    126 impl<N: Handler> Handler for Manager<N> {
    127     fn handle(&self, amount: f64) {
    128         if amount <= 1_000.0 {
    129             println!("Manager approves ${amount}");
    130         } else {
    131             self.next.handle(amount); // static dispatch!
    132         }
    133     }
    134 }
    135 
    136 struct Director<N: Handler> {
    137     next: N,
    138 }
    139 
    140 impl<N: Handler> Handler for Director<N> {
    141     fn handle(&self, amount: f64) {
    142         if amount <= 5_000.0 {
    143             println!("Director approves ${amount}");
    144         } else {
    145             self.next.handle(amount);
    146         }
    147     }
    148 }
    149 
    150 fn main() {
    151     // The full type is Manager<Director<President>>
    152     let chain = Manager {
    153         next: Director {
    154             next: President,
    155         },
    156     };
    157 
    158     chain.handle(500.0);    // Manager approves
    159     chain.handle(3_000.0);  // Director approves
    160     chain.handle(8_000.0);  // President approves
    161     chain.handle(15_000.0); // Request denied
    162 }
    163 ```
    164 
    165 The compiler monomorphizes this — it generates a unique, optimized function for
    166 each concrete type combination. No heap, no vtable.
    167 
    168 #### Using impl Trait to Hide the Chain Type
    169 
    170 If you want to hide the ugly Manager<Director<President>> type from callers, use
    171 impl Trait as a return type:
    172 
    173 ```rust
    174 fn build_chain() -> impl Handler {
    175     Manager {
    176         next: Director {
    177             next: President,
    178         },
    179     }
    180 }
    181 
    182 fn main() {
    183     let chain = build_chain(); // type is opaque to the caller
    184     chain.handle(3_000.0);
    185 }
    186 ```
    187 
    188 This still uses static dispatch under the hood — the compiler knows the exact
    189 type, the caller just doesn't need to spell it out.
    190 
    191 ### Example 3: Enum-Based Variant (More Idiomatic)
    192 
    193 Rust's enums offer a cleaner alternative when the set of handlers is fixed at
    194 compile time:
    195 
    196 ```rust
    197 enum SupportLevel {
    198     Basic,
    199     Intermediate,
    200     Critical,
    201 }
    202 
    203 fn handle_request(level: SupportLevel) {
    204     match level {
    205         SupportLevel::Basic       => println!("L1 Support handled it"),
    206         SupportLevel::Intermediate => println!("L2 Support handled it"),
    207         SupportLevel::Critical    => println!("L3 Support handled it"),
    208     }
    209 }
    210 ```
    211 
    212 This approach avoids heap allocation and dynamic dispatch entirely, but loses
    213 the runtime flexibility of adding or reordering handlers.