11"""
22*What is this pattern about?
33
4- The Chain of responsibility is an object oriented version of the
5- `if ... elif ... elif ... else ...` idiom, with the
6- benefit that the condition–action blocks can be dynamically rearranged
7- and reconfigured at runtime.
8-
9- This pattern aims to decouple the senders of a request from its
10- receivers by allowing request to move through chained
11- receivers until it is handled.
12-
13- Request receiver in simple form keeps a reference to a single successor.
14- As a variation some receivers may be capable of sending requests out
15- in several directions, forming a `tree of responsibility`.
4+ The Chain of Responsibility is an object-oriented version of the
5+ `if ... elif ... elif ... else ...` idiom, with the benefit that
6+ condition-action blocks can be dynamically rearranged and reconfigured
7+ at runtime.
8+
9+ This pattern aims to decouple senders of a request from its receivers
10+ by allowing the request to move through chained receivers until it is
11+ handled. If no handler claims it, a fallback fires — requests are
12+ never silently dropped.
13+
14+ *Real-world example — customer support escalation:
15+ L1 Support → handles common issues (billing, password reset)
16+ L2 Support → handles technical problems (bugs, integrations)
17+ L3 Support → handles complex/escalated cases (architecture, security)
18+ Fallback → logs unresolved tickets or raises a production alert
19+
20+ Each level tries to resolve the ticket. If it cannot, it escalates to
21+ the next. The fallback guarantees no ticket silently disappears.
22+
23+ *Fallback mechanism:
24+ Handler.handle() is a Template Method that owns the full dispatch flow:
25+ 1. check_range() — let this handler attempt to process the request
26+ 2. successor.handle() — if unhandled, delegate to the next in chain
27+ 3. handle_fallback() — if no successor, this always fires automatically
28+
29+ Use FallbackHandler for explicit control at the end of a chain:
30+ FallbackHandler(mode="log") — prints warning, returns False (default)
31+ FallbackHandler(mode="strict") — raises ValueError (production systems)
32+
33+ Without an explicit FallbackHandler, the base Handler.handle_fallback()
34+ no-op fires — no crash, returns False, request silently ignored.
1635
1736*Examples in Python ecosystem:
1837Django Middleware: https://docs.djangoproject.com/en/stable/topics/http/middleware/
19- The middleware components act as a chain where each processes the request/response.
38+ Each middleware component processes the request/response in sequence.
39+ Django's built-in 404/500 error views act as the chain's fallback.
2040
2141*TL;DR
22- Allow a request to pass down a chain of receivers until it is handled.
42+ Allow a request to pass down a chain of handlers. The fallback mechanism
43+ guarantees the chain never silently swallows an unhandled request.
2344"""
2445
2546from abc import ABC , abstractmethod
@@ -30,21 +51,34 @@ class Handler(ABC):
3051 def __init__ (self , successor : Optional ["Handler" ] = None ):
3152 self .successor = successor
3253
33- def handle (self , request : int ) -> None :
54+ def handle (self , request : int ) -> bool :
55+ """
56+ Template Method: attempt handling, delegate up the chain, or fall back.
57+
58+ Returns True if a concrete handler processed the request,
59+ False if the request reached the end of the chain unhandled.
60+ Delegates to handle_fallback() if no successor is set.
61+ """
62+ if self .check_range (request ):
63+ return True
64+ if self .successor :
65+ return self .successor .handle (request )
66+ return self .handle_fallback (request )
67+
68+ def handle_fallback (self , request : int ) -> bool :
3469 """
35- Handle request and stop.
36- If can't - call next handler in chain.
70+ Called automatically when the chain exhausts without handling request.
3771
38- As an alternative you might even in case of success
39- call the next handler.
72+ Override this in a subclass to customize end-of-chain behavior
73+ (e.g., write to a dead-letter queue, send an alert, log metrics).
74+ Subclasses may also raise instead of returning False (see FallbackHandler strict mode).
75+ Default implementation is a silent no-op that returns False.
4076 """
41- res = self .check_range (request )
42- if not res and self .successor :
43- self .successor .handle (request )
77+ return False
4478
4579 @abstractmethod
4680 def check_range (self , request : int ) -> Optional [bool ]:
47- """Compare passed value to predefined interval """
81+ """Return True if request was handled, None/False to pass it along. """
4882
4983
5084class ConcreteHandler0 (Handler ):
@@ -61,7 +95,7 @@ def check_range(request: int) -> Optional[bool]:
6195
6296
6397class ConcreteHandler1 (Handler ):
64- """... With it's own internal state"""
98+ """... With its own internal state"""
6599
66100 start , end = 10 , 20
67101
@@ -88,8 +122,44 @@ def get_interval_from_db() -> Tuple[int, int]:
88122
89123
90124class FallbackHandler (Handler ):
91- @staticmethod
92- def check_range (request : int ) -> Optional [bool ]:
125+ """
126+ Terminal handler for explicit end-of-chain fallback control.
127+
128+ Place at the end of a chain to define what happens when no concrete
129+ handler processes a request. Supports two modes set at construction:
130+
131+ mode="log" (default) — prints a warning and returns False.
132+ Useful during development and for monitoring.
133+ mode="strict" — raises ValueError. Use in production systems
134+ where an unhandled request is always a bug.
135+
136+ Real-world analogy — support ticket escalation:
137+ l3 = ConcreteHandler2(FallbackHandler(mode="strict"))
138+ l2 = ConcreteHandler1(l3)
139+ l1 = ConcreteHandler0(l2)
140+ l1.handle(ticket_priority) # raises ValueError if nobody claims it
141+
142+ To extend: subclass FallbackHandler and override handle_fallback() to add
143+ custom logic such as writing to a dead-letter queue or sending an alert.
144+
145+ Do not assign a successor to FallbackHandler — it is the terminal node.
146+ If a successor is set, handle_fallback() will never be called.
147+ """
148+
149+ def __init__ (self , mode : str = "log" ) -> None :
150+ super ().__init__ ()
151+ if mode not in ("log" , "strict" ):
152+ raise ValueError (
153+ f"Invalid mode { mode !r} . Choose 'log' or 'strict'."
154+ )
155+ self .mode = mode
156+
157+ def check_range (self , request : int ) -> Optional [bool ]:
158+ return None # FallbackHandler never handles requests directly
159+
160+ def handle_fallback (self , request : int ) -> bool :
161+ if self .mode == "strict" :
162+ raise ValueError (f"No handler found for request { request } " )
93163 print (f"end of chain, no handler for { request } " )
94164 return False
95165
@@ -104,7 +174,7 @@ def main():
104174
105175 >>> requests = [2, 5, 14, 22, 18, 3, 35, 27, 20]
106176 >>> for request in requests:
107- ... h0.handle(request)
177+ ... _ = h0.handle(request)
108178 request 2 handled in handler 0
109179 request 5 handled in handler 0
110180 request 14 handled in handler 1
@@ -114,6 +184,21 @@ def main():
114184 end of chain, no handler for 35
115185 request 27 handled in handler 2
116186 request 20 handled in handler 2
187+
188+ >>> # Strict mode raises ValueError for unhandled requests:
189+ >>> h_strict = ConcreteHandler0(FallbackHandler(mode="strict"))
190+ >>> h_strict.handle(5)
191+ request 5 handled in handler 0
192+ True
193+ >>> h_strict.handle(99)
194+ Traceback (most recent call last):
195+ ...
196+ ValueError: No handler found for request 99
197+
198+ >>> # Chain without FallbackHandler returns False gracefully:
199+ >>> h_bare = ConcreteHandler0()
200+ >>> h_bare.handle(99)
201+ False
117202 """
118203
119204
0 commit comments