Chain of responsibility

Now we are going to take another look at our event systems. We want to parse information about the events that happened on the system from the log lines (text files, dumped from our HTTP application server, for example), and we want to extract this information in a convenient way.

In our previous implementation, we achieved an interesting solution that was compliant with the open/closed principle and relied on the use of the __subclasses__() magic method to discover all possible event types and process the data with the right event, resolving the responsibility through a method encapsulated on each class.

This solution worked for our purposes, and it was quite extensible, but as we'll see, this design pattern will bring additional benefits.

The idea here is that we are going to create the events in a slightly different way. Each event still has the logic to determine whether or not it can process a particular log line, but it will also have a successor. This successor is a new event, the next one in the line, that will continue processing the text line in case the first one was not able to do so. The logic is simple—we chain the events, and each one of them tries to process the data. If it can, then it just returns the result. If it can't, it will pass it to its successor and repeat, as shown in the following code:

import re

class Event:
pattern = None

def __init__(self, next_event=None):
self.successor = next_event

def process(self, logline: str):
if self.can_process(logline):
return self._process(logline)

if self.successor is not None:
return self.successor.process(logline)

def _process(self, logline: str) -> dict:
parsed_data = self._parse_data(logline)
return {
"type": self.__class__.__name__,
"id": parsed_data["id"],
"value": parsed_data["value"],
}

@classmethod
def can_process(cls, logline: str) -> bool:
return cls.pattern.match(logline) is not None

@classmethod
def _parse_data(cls, logline: str) -> dict:
return cls.pattern.match(logline).groupdict()


class LoginEvent(Event):
pattern = re.compile(r"(?P<id>d+):s+logins+(?P<value>S+)")

class LogoutEvent(Event):
pattern = re.compile(r"(?P<id>d+):s+logouts+(?P<value>S+)")

With this implementation, we create the event objects, and arrange them in the particular order in which they are going to be processed. Since they all have a process() method, they are polymorphic for this message, so the order in which they are aligned is completely transparent to the client, and either one of them would be transparent too. Not only that, but the process() method has the same logic; it tries to extract the information if the data provided is correct for the type of object handling it, and if not, it moves on to the next one in the line.

This way, we could process a login event in the following way:

>>> chain = LogoutEvent(LoginEvent())
>>> chain.process("567: login User")
{'type': 'LoginEvent', 'id': '567', 'value': 'User'}

Note how LogoutEvent received LoginEvent as its successor, and when it was asked to process something that it couldn't handle, it redirected to the correct object. As we can see from the type key on the dictionary, LoginEvent was the one that actually created that dictionary.

This solution is flexible enough, and shares an interesting trait with our previous one—all conditions are mutually exclusive. As long as there are no collisions, and no piece of data has more than one handler, processing the events in any order will not be an issue.

But what if we cannot make such an assumption? With the previous implementation, we could still change the __subclasses__() call for a list that we made according to our criteria, and that would have worked just fine. And what if we wanted that order of precedence to be determined at runtime (by the user or client, for example)? That would be a shortcoming.

With the new solution, it's possible to accomplish such requirements, because we assemble the chain at runtime, so we can manipulate it dynamically as we need to.

For example, now we add a generic type that groups both the login and logout a session event, as shown in the following code:

class SessionEvent(Event):
pattern = re.compile(r"(?P<id>d+):s+log(in|out)s+(?P<value>S+)")

If for some reason, and in some part of the application, we want to capture this before the login event, this can be done by the following chain:

chain = SessionEvent(LoginEvent(LogoutEvent()))

By changing the order, we can, for instance, say that a generic session event has a higher priority than the login, but not the logout, and so on.

The fact that this pattern works with objects makes it more flexible with respect to our previous implementation, which relied on classes (and while they are still objects in Python, they aren't excluded from some degree of rigidity).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.151.126