Kyle Edwards

Design Patterns in Python

Design patterns are architectural patterns that have been established as tried-and-true implementations of functionality common in enterprise software. They were popularized by a famous book by “The Gang of Four” (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides) called Design Patterns: Elements of Reusable Object-Oriented Software.

They have their practical usage, but much of their benefit lies in the fact that they allow programmers to express complicated designs with common language, quickly cutting through the implementation details and focus on the problem at hand.

The book was written primarily for Smalltalk and C++, and in the years since, many high-level languages have implemented patterns into the language features themselves — for example Javascript’s Event implementation draws from the Observer pattern, and Python directly implements Decorators as a language construct.

The Patterns

Creational

Structural

Behavioral

Tangential Topics

SOLID Design Principles

Introduced by Robert “Uncle Bob” Martin.

Creational Patterns

Builder

Builders create complex elements by accumulating data through methods and exposing the final object or serialized string only when complete. They can be composed of sub-builders when working with complicated, nested objects. If you want to take it a step further, you can leverage method chaining to develop an intuitive interface for your builders that reads like natural language. Python’s @property decorator is handy for making this even more elegant.

Factories

Handle complex object creation all at once. Factories are especially powerful when there are multiple ways to create the same object.

Can take the form of a:

Abstract Factory

An abstract factory is a factory that is capable of creating objects amongst a family of related classess. In Python, this can be implemented without much fuss because of duck typing, however statically typed languages (and Python apps that wish to self-document or leverage static analysis tools) will require an abstract base class.

Caveats and Opinions

Builders, especially nested builders, are seemingly most useful when tackling an object so complex that the programmer constructing it might mess it up. It incurs a slight performance cost over constructing a built-in data structure. So a trade-off must be made at some point when the overhead of creating these objects from scratch is either in too many places, or if it’s becoming troublesome.

Prototype

Prototypes are partially constructed objects that can be deep copied or reused by reference that can subsequently be customized. In Python, this can be accomplished with the copy module, and either a deep or shallow copy as needed. Seems to be most useful when combined with a factory.

Note: Alternatively, think of Javascript, where prototypes are used as a by-reference component shared by instances of a single class. In this way, prototypes can be used for either pseudo-inheritance in non-OOP languages, or to save memory by having a shared set of defaults.

Singleton

Singletons are classes that can only be instantiated once, otherwise their constructor simply returns the sole instance. They are often thought to be an antipattern. In Python, you could do this by defining the allocator method __new__. However __init__ does still get called for each instance, so it’s preferable to use other methods for enforcing the singleton pattern like decorators or metaclasses.

An alternative version of the Singleton pattern, called the Monostate, uses a shared state between all instances of a class. This is especially simple in Python because you can overwrite a class instance’s __dict__ to reference this state.

Decorator Example

def singleton(cls):
  instances = {}

  def get_instance(*args, **kwargs):
    if cls not in instances:
      instances[cls] = cls(*args, **kwargs)
    return instances[cls]
  
  return get_instance

@singleton
class SingularResource:
  pass

Metaclass Example

class Singleton(type):
  _instances = {}

  def __call__(cls, *args, **kwargs):
    if cls not in cls._instances:
      cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
    return cls._instances[cls]

class SingularResource(metaclass=Singleton):
  pass

Structural Patterns

Adapter

The Adapter is a class that re-maps the interface of one class implementation to be usable for another. One potential problem with adapters is that they can possibly create many temporary adapter objects. You can leverage memoization or caching to alleviate this.

Bridge

Bridges are a pattern meant to prevent unnecessary complexity on a set of classes that differ on one or more aspects of their featureset. This way you can decouple interface hierarchies from their implementations. Bridges seem like just a good way to structure and use subclasses.

The downside of the bridge is that you end up having to add functionality into each bridge component, which violates the Open-Close principle.

from abc import ABC

class Renderer(ABC):
  def render_circle(self, radius):
    pass

class VectorRenderer(Renderer):
  def render_circle(self, radius):
    print(f"Drawing a circle vector with radius {radius}")

class RasterRenderer(Renderer):
  def render_circle(self, radius):
    print(f"Drawing a circle raster with radius {radius}")

class Shape:
  def __init__(self, renderer):
    self.renderer = renderer
  
  def draw(self): pass
  def resize(self, factor): pass

class Circle(Shape):
  def __init__(self, renderer, radius):
    super().__init__(renderer)
    self.radius = radius
  
  def draw(self):
    self.renderer.render_circle(self.radius)

  def resize(self, factor):
    self.radius *= factor

Composite

Composite objects are objects that represent groups of objects that have the same (or similar) interface to their singular representations.

One handy way of accomplishing this in Python is to use the __iter__ method to make a single object act like a collection of one.

class Singular:
  def __iter__(self):
    yield self

class Composite(list):
  def __init__(self, num):
    for i in range(num):
      self.append(Singular())

s = Singular()
c = Composite(3)
for item in s:
  print(item)

for item in c:
  print(item)

Decorator

Decorators add additional functionality to existing classes without rewriting existing code or keeping functionality separate. The simplest way to accomplish this is to extend the class and inherit from it. Functional decorators already exist in Python with the @ syntax. Python’s functional decorators can extend both functions and classes.

Traditional decorators are implemented as classes that take the decorated class instance into the constructor. To take this a step further, you can use __getattr__, __setattr__, __delattr__, __iter__, and __next__ to mirror the interface of the decorated instances in the new class. Keep in mind that in setting these attributes, it’s fairly easy to accidentally fall into an infinite loop if you don’t access attributes using the __dict__ property. However, this is pretty much exactly what class inheritance does, so only use this if it gives you some type of usability gain or if you need to modify functionality at runtime.

Note: Python provides many ways of changing class implementations at runtime, so there may be other ways to handle decorating classes.

class DecoratorClass:
  def __init__(self, decorated):
    self.decorated = decorated
  
  def __getattr__(self, key):
    return getattr(self.__dict__["decorated"], key)

  def __setattr__(self, key, value):
    if key == "decorated":
      self.__dict__["decorated"] = value
    else:
      setattr(self.__dict__["decorated"], key, value)

  def __delattr__(self, key):
    delattr(self.__dict__["decorated"], key)

Façade

A Façade hides complex logic behind a deceptively simple interface. There are plenty of uses for this, especially if you need to supply a user-friendly client for your users.

Flyweight

The Flyweight pattern creates a class or set of classes that can use shared memory.

Proxy

Proxies change the behavior of a class but keep the interface exactly the same. Proxies, façades, adapters, and decorators are all very similar in that they restructure how we access another class, but they differ in exactly how that interaction changes.

Behavioral Patterns

Command

The Command pattern is a representation of an action containing all of the data necessary for that action to be taken. This can be a powerful tool when we need our actions to be composed, rolled back, or replayed. Commands are an obvious implementation of undo/redo features, user macros, etc… especially when encapsulated into a Composite Command object that keeps track of its position.

Note: Keep in mind the problem space when using undo/redo functionality. If the program state is cumulative, you cannot undo out of order without first undoing subsequent actions.

from abc import ABC

class Command(ABC):
  def invoke(self):
    pass

  def undo(self):
    pass

class CompositeCommand(Command, list):
  def __init__(self, items=[]):
    super().__init__()
    for item in items:
      self.append(item)
    self.position = 0
  
  def invoke(self):
    for item in self:
      item.invoke()
    self.position = len(self)
  
  def undo(self):
    for item in reversed(self):
      item.undo()
    self.position = 0

Interpreter

The Interpreter pattern is used to lex (lexically categorize) strings into sets of tokens, and then parse them in a way that’s useful in a given programmatic context. Interpreters are ubiquitous in computing, used to interpret programming languages like JavaScript or Python, handle regular expressions, or domain-specific languages. Interpreters are usually split into two separate functional components known as a lexer and a parser, however in practice, implementations tend to be much more complex and varied.

Iterator

Iterators are frequently implemented as native language features or traits of native data structures in modern languages. Iterators are collections of items that allow users to step through items and keep track of the current location in an internal state. Python allows you to create your own iterables by leveraging the Iterator protocol, so long as you define the __iter__() and __next__() dunder methods, and raise a StopIterator exception when __next__() has no more valid items to return.

However, stateful iterators can become bogged down with complex logic. Because Python treats generators as iterable, it may be more readable to simply refactor your iterator to yield out values rather than

Mediator

A Mediator is a pattern that acts as a central broker for information and communication. A client can connect and disconnect to this mediator and receive new data without other clients needing to be aware.

Memento

Mementos are classes that implement snapshot of saved state. Other classes or applications can implement an array of memento objects to enable undo and redo. Unlike commands, mementos represent state rather than changes of state. Rather than implement an undo method, commands can optionally store a memento of the previous state internally, and the undo action can restore the state to this memento object (redos would of course need to do the opposite and store the next state).

More formally, the Gang of Four book describes a memento as using three separate components: the originator, the caretaker, and the memento. The originator is the primary stateful data type, while the caretaker is responsible for storing and handling mementos. This attempts to keep the originator focused on its purpose and not violate the single responsiblity principle.

Observer

The Observer pattern is the backbone of event or pub-sub (publish-subscribe) systems. Observers can listen for changes made by observables by way of callbacks placed on various actions like updating a property or appending a child node.

Note: The pub-sub pattern differs from the observer pattern as publishers and subscribers can be blissfully unaware of each other and be made asynchronous, usually by the implementation of an event bus. These busses are typically split into channels or topics that categorize the event types, allowing subscribers to be even further decoupled from the publisher logic.

class ObservableEvent(list):
  def emit(self, *args, **kwargs):
    for item in self:
      item(*args, **kwargs)

class Game:
  def __init__(self, player_name):
    self.player_name = player_name
    self.score = 0
    self.is_won = ObservableEvent()
  
  def make_goal(self):
    self.score += 1
    if self.score >= 10:
      self.is_won.emit(self.player_name, self.score)

game = Game("John")
game.is_won.append(lambda name, score: print(f"{name} won the game with {score} points!"))

for i in range(10):
  game.make_goal()

# John won the game with 10 points!

The above example create an observable state that the observed class can use, however it’s sometimes more useful to observe an existing property. You could potentially use a metaclass or a decorator to peep into a class’s starting properties and set up a listener for each one. An interesting idea would be a full-fledged library to handle this wish support not just for setters and getters, but also complex datatypes like lists, dicts, and child objects. An extremely clumsy example is shown below:

def observable(cls):
  listeners = {}

  class Wrapper:
    def __init__(self, *args, **kwargs):
      self.wrapped = cls(*args, **kwargs)

    def __getattr__(self, name):
      if name == "wrapped":
        return self.__dict__.get("wrapped")
      return getattr(self.wrapped, name)

    def __setattr__(self, name, value):
      if name == "wrapped":
        self.__dict__["wrapped"] = value
        return
      setattr(self.wrapped, name, value)
      if name not in listeners:
        listeners[name] = []
      for callback in listeners[name]:
        callback(value)

    def add_listener(self, name, callback):
      if name not in listeners:
        listeners[name] = []
      listeners[name].append(callback)

    def remove_listener(self, name, callback):
      if name not in listeners:
        listeners[name] = []
      listeners[name].remove(callback)

  return Wrapper

@observable
class Game:
  score: int = 0

game = Game()

def print_score(value):
    print(f"The new score is {value}!")

game.add_listener("score", print_score)
game.score = 1 # "The new score is 1!"
game.score = 2 # "The new score is 2!"
game.remove_listener("score", print_score)
game.score = 3 # Nothing logged

State

The State pattern is an object-oriented representation of a finite state machine. Finite state machines are machines with a limited number of states that change their state according to a set of input values. In traditional FSMs, the state is responsible for its transition to the next state, however this kind of tight coupling is frowned upon in software architecture.

Front-end frameworks like XState and Redux bring the state pattern to the front-end, although Redux complicates this somewhat by having reducers, actions, and sagas initiate changes to state, which then funnels down to UI components via React.

Strategy

The Strategy pattern is a way to store application behavior that can be selected and composed at runtime. This can be used to define an algorithm that is composible into many strategies that can be swapped out or modified as needed. This may be useful if certain components of an algorithm need to be swapped out on the fly, either as a result of certain input state, or maybe to react to an external resource being inaccessible.

Template

Templates are similar to strategies in that they are ways to organize code around complex algorithms, however templates store the entire set of logic in a superclass that can then be extended or overriden in subclasses.

Visitor

Visitors are components that can traverse complex data structures to perform some sort of action. The obvious downside to visitors is that they must be more tightly coupled to the structures they operate on than most other design patterns, but they can lessen code complexity.

The original Gang of Four implementation relies on the concept of “double dispatch” (using both a visit and an accept method), but this does require you to implement a smaller code change on each of the node subclasses. In strongly typed languages, this is necessary when you cannot be certain the exact subclass and the visit method defaults to the superclass implementation.

class Node { ... }
class SubnodeA : Node { ... }
class SubnodeB : Node { ... }

void visit(Node n) { ... }
void visit(SubnodeA a) { ... }
void visit(SubnodeB b) { ... }

for (int i = 0; i < nodes; i++) {
  Node n = nodes[i];
  visit(n); // Always calls visit(Node)
  
  // Whereas a method like this would always call the
  // correct subclass implementation:
  n.accept(visitor)
}

Note: Perhaps it’s possible to leverage C++ virtual methods for avoiding double dispatch.