At my work this summer I've had the privilege to work on a code base that was very well-designed. As a result, I feel I have substantially grown as a developer. One of the neatest parts of the project I've been working on is that there are these magical things called delegates that glue the code together. These magical entities are what I want to talk about here.
Before we begin however, I want to define something called a
functor
. For our purposes, a functor is any kind of callable object: a function, class method, or even a class
with the
__call__()
magic method defined.
Suppose you have a piece of software that looks like the following:
Delegates are what handle the flow of data from one component to the next. All we do with the implementation here is set up each individual component and then choose who is subscribed to what. Then all we do is pipe data in and watch it come out the other side.
To be sure, delegates have other use cases, but this is what I was exposed to, and it was nifty enough that I wanted to take a crack at my own implementation. It was simple enough that, not including the substantial amount time I spent on a trivial misunderstanding, I only spent an hour on the implementation, plus another for some polish.
Before I show you the implementation, I want to show you how it's used. Here's a simple example that calculates the squares of the first ten even numbers. Delegates are overkill for such a simple task, but I wanted something easy to understand. Here's a diagram of what exactly is happening:
The end result is that of applying the function \(\displaystyle f(n) = \left(2(n + 1)\right)^2\).
Here's how it might be implemented:
#!/usr/bin/python3
from modifiers import Multiplier, Exponentiator, Incrementer
from observer import Observer
def main():
# Modifiers that act on numbers.
incrementer = Incrementer(1)
doubler = Multiplier(2)
squarer = Exponentiator(2)
# An observer to view intermediate and/or final values in the delegate chain.
observer = Observer()
# Subscribe the doubler modifier to the output of the incrementer.
incrementer.subscribe(doubler)
# Subscribe the squarer modifier to the output of the doubler.
doubler.subscribe(squarer)
# Subscribe the observer to the output of the squarer.
squarer.subscribe(observer)
for i in range(10):
# Call the first modifier in the chain, and let the delegates handle the rest
incrementer(i)
# Unsubscribe functors.
incrementer.unsubscribe(doubler)
doubler.unsubscribe(squarer)
squarer.unsubscribe(observer)
if __name__ == '__main__':
main()
I've included an
Observer
object in the chain because we need some way to view the result of our calculations, which are as
follows:
~ $ ./main-simple.py
Observed value(s): 4
Observed value(s): 16
Observed value(s): 36
Observed value(s): 64
Observed value(s): 100
Observed value(s): 144
Observed value(s): 196
Observed value(s): 256
Observed value(s): 324
Observed value(s): 400
Now, as it stands, each of the modifier classes are implemented as callable objects. This is not necessary, I chose to do so for the convenience. We could do something like the following:
#!/usr/bin/python3
from delegate import Delegated
from modifiers import Exponentiator, Incrementer
from observer import Observer
class Doubler(Delegated):
"""A delegated class to double numbers"""
def __init__(self):
super().__init__()
def double(self, val):
"""A method to double the given value"""
self.delegate(2 * val)
def main():
incrementer = Incrementer(1)
# Equivalent to Multiplier(2)
doubler = Doubler()
squarer = Exponentiator(2)
observer = Observer()
incrementer.subscribe(doubler.double)
doubler.subscribe(squarer)
squarer.subscribe(observer)
for i in range(10):
# Call the first modifier in the chain, and let the delegates handle the rest
incrementer(i)
incrementer.unsubscribe(doubler.double)
doubler.unsubscribe(squarer)
squarer.unsubscribe(observer)
if __name__ == '__main__':
main()
Now for the implementation. I think we'll start with the simplest object: the
Observer
class.
Observer
inherits from
Delegated
, which provides a base constructor,
subscribe()
, and
unsubscribe()
. We call the base class constructor, and then we define the
__call__()
method.
__call__()
takes in arbitrary arguments, prints them, and the passes them on unchanged to the delegate.
from delegate import Delegated
class Observer(Delegated):
"""An Observer callable object to observe values in a Delegate chain."""
def __init__(self):
super().__init__()
def __call__(self, *things):
"""Observes given values and passes them along with no changes."""
print('Observed value(s):', *things)
self.delegate(*things)
The next thing to look at is the modifier classes:
from delegate import Delegated
class Multiplier(Delegated):
"""A Multiplier modifier to multiply numbers"""
def __init__(self, multiplier=2):
"""Construct a Multiplier object with an optional multiplier. Defaults to 2."""
super().__init__()
self.multiplier = multiplier
def __call__(self, num):
"""Multiplies a given number by a predetermined multiplier"""
self.delegate(num * self.multiplier)
class Incrementer(Delegated):
"""An Incrementer modifier to increment numbers"""
def __init__(self, increment_value=1):
"""Construct an Incrementer object with an optional increment value. Defaults to 1."""
super().__init__()
self.increment_value = increment_value
def __call__(self, num):
"""Increment num by a predetermined value"""
self.delegate(num + self.increment_value)
class Exponentiator(Delegated):
"""An Exponentiator modifier to exponentiate numbers"""
def __init__(self, power=2):
"""Construct an Exponentiator object with an optional power. Defaults to 2."""
super().__init__()
self.power = power
def __call__(self, num):
"""Exponentiates a given number by a predetermine value"""
self.delegate(num ** self.power)
These are fairly self explanatory: instead of passing on what they are given unchanged, they modify their input somehow before calling their delegates on the values.
Now for the implementation of the
Delegated
base class. A
Delegated
class has two methods:
subscribe()
and
unsubscribe()
to, appropriately, subscribe and unsubscribe from its output.
class Delegate(object):
"""A class to implement delegates"""
def __init__(self):
self.functors = list()
def subscribe(self, functor):
"""Subscribe a given functor to this delegate."""
# Verify that functor is callable. If not, don't subscribe.
if callable(functor):
self.functors.append(functor)
return self
def unsubscribe(self, functor):
"""Unsubscribe a given functor to this delegate."""
try:
self.functors.remove(functor)
# Functor wasn't subscribed.
except ValueError:
pass
return self
def __call__(self, *args):
"""Call each of the subscribed functors with the given values"""
for functor in self.functors:
functor(*args)
class Delegated(object):
"""A base class for delegated classes"""
def __init__(self):
self.delegate = Delegate()
def subscribe(self, functor):
"""Subscribes the given functor to the output of self"""
self.delegate.subscribe(functor)
def unsubscribe(self, functor):
"""Unsubscribes the given functor from the output of self"""
self.delegate.unsubscribe(functor)
The
Delegate
class is where the magic happens. What I've defined a
Delegate
to be is a list of functors, all of which will be called on the given value(s) when the delegate is
called. This way a
Delegated
class has
one
delegate, but an
arbitrary
number of subscribers.
I've shown how delegates can be used in a very simple manner, but please realize, the magic between the input and the output could be extremely complex. Perhaps each component in a piece of software is in fact complex enough to require subcomponents glued together with delegates.
One of the neat things about delegates is it allows you to insert an arbitrary component in the middle of a complex system with minimal wrangling.