Example of maintainability perils for not following the open/closed principle

Let's begin with an example of a system that is designed in such a way that does not follow the open/closed principle, in order to see the maintainability problems this carries, and the inflexibility of such a design.

The idea is that we have a part of the system that is in charge of identifying events as they occur in another system, which is being monitored. At each point, we want this component to identify the type of event, correctly, according to the values of the data that was previously gathered (for simplicity, we will assume it is packaged into a dictionary, and was previously retrieved through another means such as logs, queries, and many more). We have a class that, based on this data, will retrieve the event, which is another type with its own hierarchy.

A first attempt to solve this problem might look like this:

# openclosed_1.py
class Event:
def __init__(self, raw_data):
self.raw_data = raw_data


class UnknownEvent(Event):
"""A type of event that cannot be identified from its data."""


class LoginEvent(Event):
"""A event representing a user that has just entered the system."""


class LogoutEvent(Event):
"""An event representing a user that has just left the system."""


class SystemMonitor:
"""Identify events that occurred in the system."""

def __init__(self, event_data):
self.event_data = event_data

def identify_event(self):
if (
self.event_data["before"]["session"] == 0
and self.event_data["after"]["session"] == 1
):
return LoginEvent(self.event_data)
elif (
self.event_data["before"]["session"] == 1
and self.event_data["after"]["session"] == 0
):
return LogoutEvent(self.event_data)

return UnknownEvent(self.event_data)

The following is the expected behavior of the preceding code:

>>> l1 = SystemMonitor({"before": {"session": 0}, "after": {"session": 1}})
>>> l1.identify_event().__class__.__name__
'LoginEvent'

>>> l2 = SystemMonitor({"before": {"session": 1}, "after": {"session": 0}})
>>> l2.identify_event().__class__.__name__
'LogoutEvent'

>>> l3 = SystemMonitor({"before": {"session": 1}, "after": {"session": 1}})
>>> l3.identify_event().__class__.__name__
'UnknownEvent'

We can clearly notice the hierarchy of event types, and some business logic to construct them. For instance, when there was no previous flag for a session, but there is now, we identify that record as a login event. Conversely, when the opposite happens, it means that it was a logout event. If it was not possible to identify an event, an event of type unknown is returned. This is to preserve polymorphism by following the null object pattern (instead of returning None, it retrieves an object of the corresponding type with some default logic). The null object pattern is described in Chapter 9Common Design Patterns.

This design has some problems. The first issue is that the logic for determining the types of events is centralized inside a monolithic method. As the number of events we want to support grows, this method will as well, and it could end up being a very long method, which is bad because, as we have already discussed, it will not be doing just one thing and one thing well.

On the same line, we can see that this method is not closed for modification. Every time we want to add a new type of event to the system, we will have to change something in this method (not to mention, that the chain of elif statements will be a nightmare to read!).

We want to be able to add new types of event without having to change this method (closed for modification). We also want to be able to support new types of event (open for extension) so that when a new event is added, we only have to add code, not change the code that already exists.

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

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