Subject-based style

The subject-based style of event programming is a natural extension of unicast callback-based event handling. In this style of programming, event emitters (subjects) allow other objects to subscribe/register for notifications about their events. In practice, this is very similar to callback-based style, as event emitters usually store a list of functions or methods to call when some new event happens.

In subject-based event programming, the focus moves from the event to the subject (event emitter). The most common emanation of that style is the Observer design pattern. We will discuss the Observer design pattern in detail in Chapter 17Useful Design Patterns, but it is so important that we can't discuss subject-based event programming without introducing it here. So, we will take a sneak peek now just to see how it compares to the callback-based event programming and will discuss the details of that pattern in the next chapter.

In short, the Observer design pattern consists of two classes of objects – observers and subjects (sometimes observable). Subject is an object that maintains a list of Observer that are interested in what happens to Subject. So, Subject is an event emitter and Observer are event handlers.

As simple dummy implementation of the Observer pattern could be as follows:

class Subject:
def __init__(self):
self._observers = []

def register(self, observer):
self._observers.append(observer)

def _notify_observers(self, event):
for observer in self._observers:
observer.notify(self, event)


class Observer:
def notify(self, subject, event):
print(f"Received event {event} from {subject}")

The preceding classes are, of course, just a scaffolding. The _notify_observers() method is supposed to be called internally in the Subject class whenever something happens that could be interesting to registered observers. This can be any event, but usually subjects inform their observers about their important state changes.

Just for illustration purposes, let's assume that subjects notify all of their subscribed observers about new observers registering. Here are the updated Observer and Subject classes, which are intended to show the process of event handling:

import itertools


class Subject:
_new_id = itertools.count(1)

def __init__(self):
self._id = next(self._new_id)
self._observers = []

def register(self, observer):
self._notify_observers(f"register({observer})")
self._observers.append(observer)

def _notify_observers(self, event):
for observer in self._observers:
observer.notify(self, event)

def __str__(self):
return f"<{self.__class__.__name__}: {self._id}>"


class Observer:
_new_id = itertools.count(1)

def __init__(self):
self._id = next(self._new_id)

def notify(self, subject, event):
print(f"{self}: received event '{event}' from {subject}")

def __str__(self):
return f"<{self.__class__.__name__}: {self._id}>"

If you try to instantiate and bind the preceding classes in an interactive interpreter session, you may see the following output:

>>> from subject_based_events import Subject
>>> subject = Subject()
>>> observer1 = Observer()
>>> observer2 = Observer()
>>> observer3 = Observer()
>>> subject.register(observer1)
>>> subject.register(observer2)
<Observer: 1>: received event 'register(<Observer: 2>)' from <Subject: 1>
>>> subject.register(observer3)
<Observer: 1>: received event 'register(<Observer: 3>)' from <Subject: 1>
<Observer: 2>: received event 'register(<Observer: 3>)' from <Subject: 1>

Subject-based event programming allows for multicast event handling. This type of handling is allowed for more reusable and fine-grained event handlers with visible benefits to software modularity. Unfortunately, the change of focus from events to subjects can become a burden. In our example, observers will be notified about every event emitted from the Subject class. They have no option to register for only specific types of events. With many subjects and subscribers, this can quickly become a problem. It is either the observer that must filter all incoming events or the subject that should allow observers to register for specific events at the source. The first approach will be inefficient if the amount of events filtered out by every subscriber is large enough. The second approach may make the observer registration and event dispatch overly complex.

Despite the finer granularity of handlers and multicast capabilities, the subject-based approach to event programming rarely makes the application components more loosely coupled than the callback-based approach. This is why it isn't a good choice for the overall architecture of large applications, but rather a tool for specific problems. It's mostly due to the focus on subjects that requires all handlers to maintain a lot of assumptions about the observed subjects. Also, in the implementation of that style (that is, the Observer design pattern), both observers and subjects must, at one point of time, meet in the same context. In other words, observers cannot register to events if there is no actual subject that would emit them.

Fortunately, there is a style of event-driven programming that allows fine-grained multicast event handling in a way that really fosters loose coupling of large applications. It is a topic-based style and is a natural evolution of subject-based event programming.

In the next section, we will take a look at topic-based style.