Skip to content

Commit a82a825

Browse files
authored
Merge pull request faif#253 from gyermolenko/issue252_refactor_chain_pattern
Refactor chain pattern
2 parents 887582c + 43d1369 commit a82a825

1 file changed

Lines changed: 70 additions & 150 deletions

File tree

behavioral/chain.py

Lines changed: 70 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -3,192 +3,112 @@
33

44
"""
55
*What is this pattern about?
6-
This pattern aims to decouple the senders of a request from its
7-
receivers. It does this by allowing a request to move through chained
8-
objects until it is handled by an appropriate receiver.
96
10-
This is useful as it reduces the number of connections between objects,
11-
since the sender does not need explicit knowledge of the handler, and
12-
the receiver won't need to refer to all potential receivers, but keeps
13-
a reference to a single successor.
7+
The Chain of responsibility is an object oriented version of the
8+
`if ... elif ... elif ... else ...` idiom, with the
9+
benefit that the condition–action blocks can be dynamically rearranged
10+
and reconfigured at runtime.
11+
12+
This pattern aims to decouple the senders of a request from its
13+
receivers by allowing request to move through chained
14+
receivers until it is handled.
1415
15-
*References:
16-
http://www.dabeaz.com/coroutines/
16+
Request receiver in simple form keeps a reference to a single successor.
17+
As a variation some receivers may be capable of sending requests out
18+
in several directions, forming a `tree of responsibility`.
1719
1820
*TL;DR80
19-
Allow a request to pass down a chain of objects until an object handles
20-
the request.
21+
Allow a request to pass down a chain of receivers until it is handled.
2122
"""
2223

23-
from contextlib import contextmanager
24-
import os
25-
import sys
26-
import time
2724
import abc
2825

2926

3027
class Handler(object):
3128
__metaclass__ = abc.ABCMeta
3229

3330
def __init__(self, successor=None):
34-
self._successor = successor
31+
self.successor = successor
3532

3633
def handle(self, request):
37-
res = self._handle(request)
38-
if not res:
39-
self._successor.handle(request)
40-
41-
@abc.abstractmethod
42-
def _handle(self, request):
43-
raise NotImplementedError('Must provide implementation in subclass.')
44-
34+
"""
35+
Handle request and stop.
36+
If can't - call next handler in chain.
4537
46-
class ConcreteHandler1(Handler):
47-
def _handle(self, request):
48-
if 0 < request <= 10:
49-
print('request {} handled in handler 1'.format(request))
50-
return True
38+
As an alternative you might even in case of success
39+
call the next handler.
40+
"""
41+
res = self.check_range(request)
42+
if not res and self.successor:
43+
self.successor.handle(request)
5144

45+
@abc.abstractmethod
46+
def check_range(self, request):
47+
"""Compare passed value to predefined interval"""
5248

53-
class ConcreteHandler2(Handler):
54-
def _handle(self, request):
55-
if 10 < request <= 20:
56-
print('request {} handled in handler 2'.format(request))
57-
return True
5849

50+
class ConcreteHandler0(Handler):
51+
"""Each handler can be different.
52+
Be simple and static...
53+
"""
5954

60-
class ConcreteHandler3(Handler):
61-
def _handle(self, request):
62-
if 20 < request <= 30:
63-
print('request {} handled in handler 3'.format(request))
55+
@staticmethod
56+
def check_range(request):
57+
if 0 <= request < 10:
58+
print("request {} handled in handler 0".format(request))
6459
return True
6560

6661

67-
class DefaultHandler(Handler):
68-
def _handle(self, request):
69-
print('end of chain, no handler for {}'.format(request))
70-
return True
71-
72-
73-
class Client(object):
74-
def __init__(self):
75-
self.handler = ConcreteHandler1(ConcreteHandler3(ConcreteHandler2(DefaultHandler())))
76-
77-
def delegate(self, requests):
78-
for request in requests:
79-
self.handler.handle(request)
80-
81-
82-
def coroutine(func):
83-
def start(*args, **kwargs):
84-
cr = func(*args, **kwargs)
85-
next(cr)
86-
return cr
87-
88-
return start
89-
90-
91-
@coroutine
92-
def coroutine1(target):
93-
while True:
94-
request = yield
95-
if 0 < request <= 10:
96-
print('request {} handled in coroutine 1'.format(request))
97-
else:
98-
target.send(request)
99-
100-
101-
@coroutine
102-
def coroutine2(target):
103-
while True:
104-
request = yield
105-
if 10 < request <= 20:
106-
print('request {} handled in coroutine 2'.format(request))
107-
else:
108-
target.send(request)
109-
110-
111-
@coroutine
112-
def coroutine3(target):
113-
while True:
114-
request = yield
115-
if 20 < request <= 30:
116-
print('request {} handled in coroutine 3'.format(request))
117-
else:
118-
target.send(request)
119-
120-
121-
@coroutine
122-
def default_coroutine():
123-
while True:
124-
request = yield
125-
print('end of chain, no coroutine for {}'.format(request))
62+
class ConcreteHandler1(Handler):
63+
"""... With it's own internal state"""
12664

65+
start, end = 10, 20
12766

128-
class ClientCoroutine:
129-
def __init__(self):
130-
self.target = coroutine1(coroutine3(coroutine2(default_coroutine())))
67+
def check_range(self, request):
68+
if self.start <= request < self.end:
69+
print("request {} handled in handler 1".format(request))
70+
return True
13171

132-
def delegate(self, requests):
133-
for request in requests:
134-
self.target.send(request)
13572

73+
class ConcreteHandler2(Handler):
74+
"""... With helper methods."""
13675

137-
def timeit(func):
138-
def count(*args, **kwargs):
139-
start = time.time()
140-
res = func(*args, **kwargs)
141-
count._time = time.time() - start
142-
return res
76+
def check_range(self, request):
77+
start, end = self.get_interval_from_db()
78+
if start <= request < end:
79+
print("request {} handled in handler 2".format(request))
80+
return True
14381

144-
return count
82+
@staticmethod
83+
def get_interval_from_db():
84+
return (20, 30)
14585

14686

147-
@contextmanager
148-
def suppress_stdout():
149-
try:
150-
stdout, sys.stdout = sys.stdout, open(os.devnull, 'w')
151-
yield
152-
finally:
153-
sys.stdout = stdout
87+
class FallbackHandler(Handler):
88+
@staticmethod
89+
def check_range(request):
90+
print("end of chain, no handler for {}".format(request))
91+
return False
15492

15593

15694
if __name__ == "__main__":
157-
client1 = Client()
158-
client2 = ClientCoroutine()
159-
requests = [2, 5, 14, 22, 18, 3, 35, 27, 20]
95+
h0 = ConcreteHandler0()
96+
h1 = ConcreteHandler1()
97+
h2 = ConcreteHandler2(FallbackHandler())
98+
h0.successor = h1
99+
h1.successor = h2
160100

161-
client1.delegate(requests)
162-
print('-' * 30)
163-
client2.delegate(requests)
164-
165-
requests *= 10000
166-
client1_delegate = timeit(client1.delegate)
167-
client2_delegate = timeit(client2.delegate)
168-
with suppress_stdout():
169-
client1_delegate(requests)
170-
client2_delegate(requests)
171-
# lets check which is faster
172-
print(client1_delegate._time, client2_delegate._time)
101+
requests = [2, 5, 14, 22, 18, 3, 35, 27, 20]
102+
for request in requests:
103+
h0.handle(request)
173104

174105
### OUTPUT ###
175-
# request 2 handled in handler 1
176-
# request 5 handled in handler 1
177-
# request 14 handled in handler 2
178-
# request 22 handled in handler 3
179-
# request 18 handled in handler 2
180-
# request 3 handled in handler 1
106+
# request 2 handled in handler 0
107+
# request 5 handled in handler 0
108+
# request 14 handled in handler 1
109+
# request 22 handled in handler 2
110+
# request 18 handled in handler 1
111+
# request 3 handled in handler 0
181112
# end of chain, no handler for 35
182-
# request 27 handled in handler 3
113+
# request 27 handled in handler 2
183114
# request 20 handled in handler 2
184-
# ------------------------------
185-
# request 2 handled in coroutine 1
186-
# request 5 handled in coroutine 1
187-
# request 14 handled in coroutine 2
188-
# request 22 handled in coroutine 3
189-
# request 18 handled in coroutine 2
190-
# request 3 handled in coroutine 1
191-
# end of chain, no coroutine for 35
192-
# request 27 handled in coroutine 3
193-
# request 20 handled in coroutine 2
194-
# (0.2369999885559082, 0.16199994087219238)

0 commit comments

Comments
 (0)