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.