Misusing Software Design Patterns?

Nassir Al-Khishman
36 min readOct 5, 2024

--

A software design pattern is a general solution that is worth reusing on a commonly encountered problem.

The generality of these patterns can lead to many different ways of implementing them. In fact, implementation needed to evolve to keep up with organizational needs and preferences. Some common examples come to mind:

  1. functional programming allows making classless callbacks that can serve as commands in the command pattern
  2. the observer pattern has been adopted into pub-sub systems in microservice architectures patterns have been adopted into microservice architectures:

In contrast, it is not okay to apply software design patterns when the problem does not exist. This is because the pattern is saying “The problem exists!” but the engineer and AI assistant are saying “Where?”. This confusion indicates a reduction in readability and therefore organizational productivity.

For this reason, we are assembling this blog post to share non-target problems and the targeted problems by the original gang of four patterns. To come up with examples, we drew from practical applications in modern web and machine learning development.

A repository of the reference can be found on https://github.com/Abahope/oop_design_patterns.

Quiz for Gang of Four Patterns

If you want to challenge yourself before using the reference, try this multiple-choice quiz: https://www.abahope.com/design_patterns_quiz

Gang of Four Patterns

Creational

Creational patterns are used for instantiating instances. A prerequisite for using these is that an instance needs to be created.

Factory Method

Description: Provide a creator interface for creating an object that implements an interface.

Factory Method (Source: http://www.mcdonaldland.info/files/designpatterns/designpatternscard.pdf).

Basic Python Implementation

# Core or library code
class Product:
"""Responsible for determining product family behaviors"""

def do_family_thing(self): ...


class Creator:
"""Responsible for unifying the interface for creating products"""

def create_product(self) -> Product: ...


# Consumer or add-on code
class ConcreteProductA(Product):
"""Unique product in a family"""

def do_family_thing(self):
print("I am ConcreteProductA and this is my special trick")


class ConcreteCreatorA(Creator):
"""Responsible for creating ConcreteProductA"""

def create_product(self) -> ConcreteProductA:
return ConcreteProductA()


# Any component can make code like this (client, core, non-core)
def some_function(unknown_creator: Creator):
"""Does some stuff

- We don't know: (1) our creator, (2) product or (3) how our product
gets created, but we can (a) delay creation up to this function and
(b) still know what our product can do
"""
unknown_concrete_product = unknown_creator.create_product()
unknown_concrete_product.do_family_thing()


if __name__ == "__main__":
some_function(ConcreteCreatorA())

Targeted Problem: Some code needs to move out knowledge of how an object should be instantiated.

Non-target-problems:

  • The object can be instantiated in advance.
  • The object’s class can be responsible for instance creation.
  • The code does not need to know what it’s creating.

Examples:

  • Backend: Django’s manager’s create. The manager is the factory. By allowing Django consumers to implement the manager’s create method, Django is able to create the DB rows while supplementing product creation with hooks for pre-creation and post-creation.
  • Frontend: Functional components in modern React. The Functional Component is an HTML factory. By making React consumers implement the functional components, React is able to create HTML while internally optimizing rendering.

Builder

Description: Separates the construction of a complex object from its representation so that the same construction process can create different representations.

Builder Pattern (Source: http://www.mcdonaldland.info/files/designpatterns/designpatternscard.pdf)

Basic Python Implementation

# Core or library code
class Builder:
"""Responsible for determining the type of parts to be built"""

def build_part_1(self): ...

def build_part_2(self): ...

def get_result(self): ...


class Director:
"""Responsible for determining what constitutes a full/lite product"""

def build_full(self, builder: Builder):
builder.build_part_1()
builder.build_part_2()
return builder.get_result()

def build_lite(self, builder: Builder):
builder.build_part_1()
return builder.get_result()


# Consumer or add-on code
class ConcreteBuilderA(Builder):
"""Responsible for determining what parts to use"""

product = []

def build_part_1(self):
print("I, ConcreteBuilderA, am going to add a special part 1")
self.product += ["1"]

def build_part_2(self):
print("I, ConcreteBuilderA, am going to add a special part 2")
self.product += ["2"]

def get_result(self):
out = self.product
self.product = []
return out


# Any component can make code like this (client, core, non-core)
def some_function(unknown_builder: Builder):
"""Does some stuff

- We don't know: (1) our builder, (2) our product or (3) how our product
gets created, but we know it's going to be a full product
"""
director = Director()
full_product = director.build_full(unknown_builder)
print(f"full_product: {full_product}")

lite_product = director.build_lite(unknown_builder)
print(f"lite_product: {lite_product}")


if __name__ == "__main__":
some_function(ConcreteBuilderA())

Targeted Problem: There is a part set for which different subsets can be used to make different representations. The code wants to move out: how parts are built and, importantly, what those parts are.

Non-target-problems:

  • Subsets of a part set can only make one representation.

Examples:

  • Physical-world example: A sandwich builder. Suppose a sandwich has three parts: (1) wrapping, (2) main, and (3) sauces. A concrete sandwich builder might have bun wrapping, burger main, and Big Mac sauces (this blog post is not endorsed by McDonald’s). The director should be able to make_a_full_sandwich, make_a_sauceless_sandwich, etc., from any builder implementer by calling a combination of build_main, build_sauces, and build_wrapping.
  • Backend: Django ORM. Django allows you to create different querysets. The queryset is the builder, and the SQL query is the product. By allowing Django consumers to build a queryset from parts such as filters and orderings, a large combination of querysets and therefore queries can be made.
  • Frontend: A custom URL builder. The URL builder can have a method that adds query parameters to allow building a large combination of URLs.
  • ML CNN: Convolutional neural network (CNN) blocks. In CNNs, it is standard to make a graph of convolutional blocks. The convolutional blocks themselves are made from a layer part set. Common layers include convolutional, dropout, max pool, and average pool.

Abstract Factory: Defines an interface for creating an object but let subclasses decide which class to instantiate.

Abstract Factory (Source: http://www.mcdonaldland.info/files/designpatterns/designpatternscard.pdf).

Basic Python Implementation

# Core or library code
class AbstractProduct:
"""Responsible for unifying behavior of all products

- Alternatively, you can create an abstract product for each type of product
ie AbstractProductA, AbstractProductB. In my opinion, that is a more convincing
usecase because it means that product A and product B are sufficiently different
to warrant different classes.
"""

...


class AbstractFactory:
"""Responsible for determining what constitutes a set of compatible products"""

def create_product_A(self) -> AbstractProduct: ...

def create_product_B(self) -> AbstractProduct: ...


# Consumer or add-on code
class ConcreteProductGreenA(AbstractProduct):
"""Product A from the Green set"""

def __repr__(self):
return "GreenA"


class ConcreteProductGreenB(AbstractProduct):
"""Product B from the Green set"""

def __repr__(self):
return "GreenB"


class ConcreteFactoryGreen(AbstractFactory):
"""Responsible for determiing how products from the Green set get created"""

def create_product_A(self) -> AbstractProduct:
return ConcreteProductGreenA()

def create_product_B(self) -> AbstractProduct:
return ConcreteProductGreenB()


# Any component can make code like this (client, core, non-core)
def add_to_basket(products: list[AbstractProduct]):
"""Adds products to a basket"""
print(f"Adding {products} to basket")


class Client:
def some_method(self, unknown_factory_a: AbstractFactory):
"""Does some stuff


- We don't know: (1) our factory, (2) our products or (3) how our product
gets created, but we know our products are going to be a set under the factory
/compatible
"""
unknown_product_a = unknown_factory_a.create_product_A()
unknown_product_b = unknown_factory_a.create_product_B()

# We know they belong together
add_to_basket([unknown_product_a, unknown_product_b])
# You can also extend the product interface(s)
# unknown_product_a.gracefully_interact_with(unknown_product_b)


if __name__ == "__main__":
client = Client()
client.some_method(ConcreteFactoryGreen())

Targeted Problem: There are multiple sets/families of products, and products from different families should not be used together. Generally, there are two factors (in the example, Factor 1: Green/Red. Factor 2: A/B)

Non-target problems:

  • Products are not related.
  • There is one factor through which products vary. Check the factory method instead.

Examples:

  • Infrastructure: Cloud-app implementations. Some organizations need to allow different cloud providers for their infrastructures. If they have various optional applications that can interact, an abstract factory is a suitable pattern. Factor 1: The factories would be AWS Factory, Google Factory, or Azure Factory. Factor 2: Each factory would implement methods such as build_app_A, build_app_B, and build_app_C. Consumers of the factories can create any app and be sure they’re all in the same cloud provider.
  • Backend: Multi-DB support. If you need to support different databases such as SQL and noSQL, Abstract Factory would be a good fit. Factor 1 is the database type. Factor 2 is the object type (in Facebook, users, posts, etc.). By using any of the factories, it’s possible to create any object and be sure they all belong to the same database.
  • Frontend: Multi-locale support. To support multiple locales, multiple items need to be translated: currency, language, time, etc. Factor 1 is the locale. Factor 2 is the type of item. By choosing a locale factory, you can be sure that all the items are consistent.

Prototype: Specifies the kinds of objects to create using a prototypical instance and creates new objects by copying this prototype.

Prototype Pattern (Source: http://www.mcdonaldland.info/files/designpatterns/designpatternscard.pdf).

Basic Python Implementation

# Core or library code
class Prototype:
"""Responsible for unifying the interface for cloning an item"""

def clone(self) -> "Prototype": ...


# Consumer or add-on code
class ConcretePrototype(Prototype):
"""Some clonable object"""

def clone(self) -> "ConcretePrototype":
print("Cloning a ConcretePrototype")
return ConcretePrototype()


# Any component can make code like this (client, core, non-core)


class Client:
def some_method(self, unknown_object: Prototype):
"""Does some stuff

- We don't know our product, but we know how to get a copy of it
"""
unknown_object_duplicate = unknown_object.clone()


if __name__ == "__main__":
client = Client()
client.some_function(ConcretePrototype())

Targeted Problem: End users need to create an instantiable class at runtime by varying state instead of coding. This runtime class is the prototype.

Non-target problems:

  • You need to create an object with the class’s default state.

Examples:

  • Backend: Organization-specific objects. A lot of backends have core objects that need to be cloned to reduce user effort. For example, a job posting site might allow users to clone their postings. In doing so, users can reduce some of the initial setup effort.
  • Frontend: Javascript/Typescript object cloning. Javascript encourages consumers to treat objects immutably, so there are many instances where an object is cloned. A common yet subtle example is the shallow copy: newObject = {…oldObject, changedField}.
  • Miscellaneous: cloning architecture diagram objects, Google Docs, etc.

Singleton: Ensure a class only has one instance and provide a global point of access to it.

Singleton Pattern (Source: http://www.mcdonaldland.info/files/designpatterns/designpatternscard.pdf).

Basic Python Implementation

# Any component can make code like this (client, core, non-core)
class Singleton:
"""Responsible for ensuring a max of one instance and providing access to it"""

_instance = None
_initialized = False

def __new__(cls, *args, **kwargs):
if not cls._instance:
print("Creating new instance")
cls._instance = super().__new__(cls)
return cls._instance

def __init__(self, *args, **kwargs):
print("Gotcha: init is called even if we do not create a new instance")
if not self._initialized:
# subscribe to a realtime data queue (represented by this stack)
self.stack = [5, 4, 3, 2, 1]
self._initialized = True


# Any component can make code like this (client, core, non-core)
class Client:
def some_method(self):
"""Does some stuff"""
singleton_instance_1 = Singleton()
assert singleton_instance_1.stack.pop() == 1
singleton_instance_2 = Singleton()
# We get the same instance as evidenced by the stack pop response
assert singleton_instance_2.stack.pop() == 2
assert singleton_instance_1.stack.pop() == 3
print("Success")


if __name__ == "__main__":
client = Client()
client.some_function()

Targeted Problem: There is a class for which you only want one instance at a time. There can be many reasons for this, but I’ve often seen it used to prevent resource wastage or to lock resources.

Non-target problems:

  • You guess it’ll save a bit of memory at no cost.
  • The objects are not resource-intensive or do not need locking.

Examples:

  • Database: Row-level exclusive write locks. When a transaction wants to write (insert, update, delete) a row, it requests an exclusive lock. To protect data integrity, locks are held until the transaction is committed or rolled back.
  • Backend: Connections to the database. Some applications may apply the singleton pattern for application-database connections. By limiting to one, resource usage may be reduced for the application and database.
  • Frontend: Websocket connections to backend servers. We use a single class instance to avoid making multiple connections between the same servers. Multiple connections between the same servers can result in unexpected behaviour and unnecessary performance costs.
  • ML models: ML model instantiation. ML models can take up a lot of memory. To reduce the memory footprint and instantiation compute cost, we usually apply a singleton pattern. One read-only instance of the model can be safely used by multiple inference threads.

Structural

Structural patterns are about creating some sort of intermediate object or structural abstraction to facilitate an interaction.

Proxy: Provides a surrogate or placeholder for another object to control access to it. Proxies often start with a focus on access control but become general request handlers as adapter functionalities are added.

Proxy pattern (Source: http://www.mcdonaldland.info/files/designpatterns/designpatternscard.pdf).

Basic Python Implementation

import random

# Core or library code


class Subject:
"""Subject interface

Responsible for defining interface of real subject,
but also used to ensure proxies implement real subject's interface
"""

def request(self): ...


class RealSubject:
"""Responsible for some behavior"""

def request(self):
print("RealSubject did something")


# Consumer or add-on code


class Proxy(Subject):
"""Responsible for controlling access to some real subject"""

def __init__(self, real_subject: RealSubject):
self.real_subject = real_subject

def additional_behavior(self):
print("Proxy did some additional behavior")

def the_stars_align(self):
true_or_false = bool(random.getrandbits(1))
if true_or_false:
print("The stars align")
else:
print("The stars do not align")
return true_or_false

def request(self):
self.additional_behavior()
if self.the_stars_align():
self.real_subject.request()


# Any component can make code like this (client, core, non-core)


def some_function(subject: Subject):
"""Does some stuff

- We don't know if we received a real subject or a proxy
"""
subject.request()


if __name__ == "__main__":
real_subject = RealSubject()
proxy = Proxy(real_subject)
some_function(proxy)

Targeted Problem: The code needs to control access to a specific object. Most of the time, the subject can be swapped out using the proxy. Still, the proxy can have its own methods.

Non-target problems:

  • The code needs to simplify access to a specific object. In this case, you just need an abstraction. You may choose to call it a proxy, but it would be clearer if you called it a wrapper or client.
  • The code needs to make a specific object fit an expectation. In this case, you might need an adapter.

Examples:

  • Cloud: Network file systems such as AWS EFS. AWS EFS is a remote central file server that machines can access as if it were their local file system. By not requiring consumers to change how they interact with files and controlling access to files with local caching, the remote file server is proxied.
  • Frontend: Although frontends can have abstraction classes such as clients, they should not have proxies because access control is not a responsibility of frontends. To be explicit, frontends operate in a context controlled by the user. Therefore, any kind of access control done purely on the frontend is inherently unreliable.
  • Backend: Proxies for minimizing the number of entry points. Some backends are required to use only one machine at a throttled rate for making all of their third-party requests. One possible solution is to make a proxy microservice that takes all the requests, authenticates them, and then executes them at the target throttle rate from one machine.
  • ML: Proxies for limiting access to an ML model. Some ML models cost a lot of computing resources. To reduce the overall compute expenditure, you may add a proxy that caches responses and reuses them in place of calling the model.

Adapter: Converts the interface of a class into another interface clients expect.

Adapter pattern, object version (Source: http://www.mcdonaldland.info/files/designpatterns/designpatternscard.pdf). FYI: There is an official class version that inherits from the adaptee instead of composing it.

Basic Python Implementation

# Core or library code
class Adapter:
"""Responsible for unifying an interface for the client"""

def operation(self): ...


# Add-on code
class Adaptee:
def adapted_operation(self):
print("Compensating operation")


class ConcreteAdapter(Adapter):
"""Responsible for adapting the adaptee to the client"""

adaptee = Adaptee()

def operation(self):
# Maybe modify input here for adaptee
print("Adapter operation")
self.adaptee.adapted_operation()


# Any component can make code like this (client, core, non-core)
class Client:
def some_method(self, adapter: Adapter):
"""Does some stuff

- We don't know our adaptee/its interface, but we know how to call it
"""
adapter.operation()


if __name__ == "__main__":
client = Client()
adapter = ConcreteAdapter()
client.some_function(adapter)

Targeted problem: You have an interface and want to add a non-interface-complying functionality.

Non-target problems:

  • Connecting your application with something already compatible.

Examples:

  • General: Adapting libraries to fit your interface. You should not have to change your interface or fork the library to treat all classes uniformly.
  • Backend: Integrating a third-party service with your application. Ideally, the services you want to integrate are using a standardized API such as REST or Kafka. This is not always the case. Furthermore, you might want to add complementary behaviour. Your application should not have to know how each third party works. This is especially true if the third party requires the usage of a legacy API. Instead, it is more maintainable to make an adapter that your application can easily call.
  • Frontend: Same thing as backend above.

Bridge: Decouples an abstraction from its implementation so that the two can vary independently

Bridge pattern (Source: http://www.mcdonaldland.info/files/designpatterns/designpatternscard.pdf).

Basic Python Implementation

# Core or library code


class Implementor:
"""Responsible for unifying an interface for the abstraction"""

def operation_1_impl(self): ...


class Abstraction1:
"""Responsible for abstracting a functionality"""

def __init__(self, implementor: Implementor):
self.implementor = implementor

def operation(self):
self.implementor.operation_1_impl()


# Consumer or add-on code
class ConcreteImplementorA(Implementor):
"""Responsible for implementing a functionality"""

def operation_1_impl(self):
print("ConcreteImplementor operation_1_impl")


# Any component can make code like this (client, non-core)
def some_function(implementor):
"""Does some stuff

- We rather not call the implementor directly because
* we do not have insight to its type or behavior,
* its interface changes regularly,
* or the interface is out of our development control...
- This way, the implementor can be changed without changing the client code
"""
abstraction = Abstraction1(implementor)
abstraction.operation()


if __name__ == "__main__":
some_function(ConcreteImplementorA())

Targeted Problem: There is an abstraction that needs stability, yet the number of subclasses multiplies every time we add a new sub-behaviour.

Non-target problems:

  • There is no abstraction
  • Adding an implementation does not multiply the number of subclasses.
  • The interface or implementor should be in our development control

Examples:

  • Web development: REST-API objects bridging to implementation in backend endpoints/views. In this pattern, the abstraction would be the REST-API standard (POST, GET, PUT, PATCH, etc.); the implementors would be our backend endpoints. These days, the implementor interface is just that the implementor is a callback with some optional input. The client can call the REST API with the URL slug instead of directly calling the backend endpoints. To accommodate an additional backend object, the frontend client does not need to adapt to N additional endpoints.
  • Libraries: The interface of a library. A lot of libraries require stability in their interface, but they might need to change their underlying implementation or add additional implementations. The consumer usually just needs to specify some implementation key.

Flyweight: Uses sharing to support large numbers of fine-grained objects efficiently.

Flyweight pattern (Source: http://www.mcdonaldland.info/files/designpatterns/designpatternscard.pdf).

Basic Python Implementation

# Core or library code


class Flyweight:
"""Responsible for allowing uniform treatment of the flyweights"""

def operation(self, extrinsic_state): ...


def does_not_change_small_unshared_state(extrinsic_state: int): ...


class ConcreteFlyweight(Flyweight):
"""Responsible for managing the large shared state

- If state changes here, it'll change state for all flyweights that use it
"""

def __init__(self, large_shared_state: str):
self._large_shared_state = large_shared_state

@property
def large_shared_state(self):
"""Allows reading the large shared state"""
return self._large_shared_state

@large_shared_state.setter
def large_shared_state(self, value):
"""Prevents changing the large shared state"""
raise AttributeError("Cannot set large_shared_state")

def operation(self, extrinsic_state: int):
print("ConcreteFlyweight does not need to respond to extrinsic state")

def __repr__(self):
original = super().__repr__()
return f"{original} shared_state: {self.large_shared_state}"


class UnsharedConcreteFlyweight(Flyweight):
"""Responsible for managing some unique state"""

def __init__(self, concrete_flyweight: ConcreteFlyweight,
unshared_state=0):
self.concrete_flyweight = concrete_flyweight
self.unshared_state = unshared_state

def operation(self, extrinsic_state: int):
if does_not_change_small_unshared_state(extrinsic_state):
return
# Now use extrinsic state to change the small unshared state
self.unshared_state += extrinsic_state
# If necessary, can also access concrete_flyweight.large_shared_state
# here

def __repr__(self):
original = super().__repr__()
return f"{original}, unshared_state: {self.unshared_state}, shared_state: {self.concrete_flyweight.large_shared_state}"


class FlyweightFactory:

def __init__(self):
self.key_to_concrete_flyweight = {}

def get_flyweight(self, key) -> Flyweight:
if key not in self.key_to_concrete_flyweight:
large_shared_state = "some large shared state"
self.key_to_concrete_flyweight[key] = ConcreteFlyweight(
large_shared_state)
return self.key_to_concrete_flyweight[key]
return UnsharedConcreteFlyweight(
concrete_flyweight=self.key_to_concrete_flyweight[key]
)


# Any component can make code like this (client, core, non-core)
def flyweight_creation(flyweight_factory: FlyweightFactory) -> list[Flyweight]:
key = "some key"
# The first one will be concrete
concrete_flyweight = flyweight_factory.get_flyweight(key=key)
# Subsequent ones will be unshared
unshared_concrete_flyweight_1 = flyweight_factory.get_flyweight(key=key)
unshared_concrete_flyweight_2 = flyweight_factory.get_flyweight(key=key)
flyweights = [
concrete_flyweight,
unshared_concrete_flyweight_1,
unshared_concrete_flyweight_2,
]
return flyweights


def manipulate_flyweights(flyweights: list[Flyweight], extrinsic_state: int):
"""Does some stuff

- We don't know which of our flyweights contain the large immutable shared state,
but we can treat them uniformly
"""
for f in flyweights:
f.operation(extrinsic_state)


if __name__ == "__main__":
flyweights = flyweight_creation(FlyweightFactory())
print("Before manipulate_flyweights")
for f in flyweights:
print(f)
manipulate_flyweights(flyweights=flyweights, extrinsic_state=1)
print("After manipulate_flyweights 1")
for f in flyweights:
print(f)

Targeted Problem: There is a need to make many uniformly-treated objects, but there is also a need to save memory. To prevent side effects and race conditions, the large shared state is usually immutable.

Non-target problems:

  • There is no need to save memory.
  • There is no need to treat the high-memory object the same as other objects. Consider a singleton for the high-memory object or state.
  • The large state needs to be mutable.

Examples:

We can make up some examples and mislead you by pretending wehave seen it before, but we have not seen any usage of this pattern in web development or ML.

We think there are 2 reasons for this:

  • It is more maintainable when a dedicated class for the small state references a dedicated object for the large state
  • State usually needs to be mutable. If the state is immutable, it is usually sufficient to use a read-only singleton for the dedicated large state class mentioned in the previous point.

Decorator: Attaches additional responsibilities to an object dynamically.

Decorator pattern (Source: http://www.mcdonaldland.info/files/designpatterns/designpatternscard.pdf).

Basic Python Implementation

# Core or library code
class Component:
"""Responsible for unifying the interface for concrete components and decorators"""

def operation(self): ...


class Decorator(Component):
"""Responsible for defining the link between the decorator and decoratee"""

def __init__(self, component: Component):
self.component = component

def operation(self): ...


# Consumer or add-on code


class ConcreteComponent(Component):
"""Responsible for some undefined behavior"""

def operation(self):
print("I can do this")


class ConcreteDecoratorN(Decorator):
"""Responsible for adding some behavior"""

def added_behavior(self):
print("I can do N")

def operation(self):
self.added_behavior()
self.component.operation()


# Any component can make code like this (client, core, non-core)


def some_function(component: Component):
"""Does some stuff

- We don't know what our component is, but we know how to add behaviors to it
- We can also take in a decorator class as input
"""
print("Initial component behavior:")
component.operation()
print("-" * 10)
print("decorating component with N")
component_v2 = ConcreteDecoratorN(component)
print("Component with N behavior:")
component_v2.operation()
# We can layer on additional behaviors
print("-" * 10)
print("decorating component with N")
component_v3 = ConcreteDecoratorN(component_v2)
print("Component with N behavior:")
component_v3.operation()


if __name__ == "__main__":
component = ConcreteComponent()
some_function(component)

Targeted Problem: There is a behaviour that can be optionally wrapped around different objects (usually simple classes or their instances). Usually, the change can wrap around the behaviour like skin, and not change the behaviour like guts (that would instead be a strategy pattern).

Non-target problems:

  • The behaviour is not reusable
  • The behaviour needs to change (instead of add-on) the wrapped object’s behaviour.

Examples:

  • Frontend: Before hook-based React became popular, it was common to create ‘higher order functions’ to wrap around classes or other functions (reference). This wrapping was supplementing the behaviour of the wrapped object.
  • Backend: In monolithic backends, it is common to add an authentication layer by wrapping the relevant endpoints with an authentication decorator. In microservices, a service is implicitly decorated by an authentication layer if it only accepts requests that already passed an authentication service.
  • ML: In practice, machine learning layers are often made by decorating previous layers. For example in Pytorch, it is common to forward propagate in this manner: x = layer_2(layer_1(x)). In doing this, it becomes easy to add and remove layers — especially dynamically.

Facade: Provides a unified interface to a set of interfaces in a subsystem.

Facade pattern (Source: http://www.mcdonaldland.info/files/designpatterns/designpatternscard.pdf).

Basic Python Implementation

# Core or library code


class ComponentA:
"""Responsible for behavior A"""

def something_unique(self):
print("ComponentA did something unique")


class ComponentB:
"""Responsible for behavior B"""

def something_interesting(self):
print("ComponentB did something interesting")


# Consumer or add-on code
class Facade:
"""Responsible for making ComponentA and ComponentB easier to use"""

def __init__(self):
self.a = ComponentA()
self.b = ComponentB()

def do_something_specific(self):
self.a.something_unique()
self.b.something_interesting()


# Any component can make code like this (client, core, non-core)


def some_function(facade: Facade):
"""Does some stuff

- We don't know what our subsystem components are or how to call them,
but we know how to talk to the facade.
"""
facade.do_something_specific()


if __name__ == "__main__":
some_function(Facade())

Targeted problem: Clients are struggling with calling multiple different components in a complex system.

Non-target problems:

  • It is okay for clients to decide which components they call.

Examples:

  • Web development: In a microservice architecture, it is common to have one frontline service that coordinates transactions over multiple other microservices. Instead of making the clients coordinate interactions with multiple microservices, that responsibility is deferred to a facade. In a monolithic architecture, it is sometimes worth making an endpoint that effectively calls multiple other RESTful endpoints.
  • ML: Ensemble ML models such as random forests. While random forests were made to improve overall prediction performance, they are a decent example of a Facade pattern. Instead of making single tree models and asking consumers to use them, random forests facade away the low-level complexity.

Composite: Composes objects into tree structures to represent part-whole hierarchies. Generally involves treating a node and its subset nodes as a whole.

Composite pattern (Source: http://www.mcdonaldland.info/files/designpatterns/designpatternscard.pdf).

Basic Python Implementation

# Core or library code
class Component:
"""Responsible for allowing uniform treatment of the hierarchy"""

def operation(self): ...

def add(self, c: "Component"): ...

def remove(self, c: "Component"): ...

def getChild(self, i: int): ...


# Consumer or add-on code
class Composite(Component):
"""Responsible for defining the hierarchy structure and propogating down behavior"""

children: list[Component] = []

def add(self, c: Component):
self.children.append(c)

def remove(self, c: Component):
self.children.remove(c)

def getChild(self, i: int):
return self.children[i]

def operation(self):
print("Composite operation")
for child in self.children:
child.operation()


class Leaf(Component):
"""Response for implementing the operation's behavior"""

def operation(self):
print("Leaf operation")

def add(self, c: Component):
raise Exception("Leafs cannot add")

def remove(self, c: Component):
raise Exception("Leafs cannot remove")

def getChild(self, i: int):
raise Exception("Leafs do not have children")


# Any component can make code like this (client, core, non-core)
def some_function(component: Component):
"""Does some stuff

- We don't know if our component is a Leaf or Composite, but we need
to call the behavior for the component (and its children if applicable)
"""
component.operation()
try:
child = component.getChild(0)
print("child", child)
except Exception as e:
print(e)


if __name__ == "__main__":
print("Leaf")
some_function(Leaf())
print("Composite")
composite = Composite()
composite.add(Leaf())
composite.add(Leaf())
some_function(composite)

Targeted Problem: There is a benefit to interacting with multiple objects as part-whole hierarchies.

Non-target problems:

  • There is no reasonable hierarchy.

Examples:

  • Database: Delete-cascading in SQL. SQL is a relational database. Children rows that must reference parents row can delete themselves before their parent is deleted. The client only has to delete one row.
  • Backend: Search in binary search trees, tries/prefix trees, and DB indices. Subset guarantees provided by the parent make the search more efficient. The client only has to search the subset’s root.
  • Frontend: React uses composition as a core design principle. The DOM trees are made by combining components in different ways instead of inheriting.
  • ML: Like most trees, a decision tree is an example of a composite pattern. To find the final decision leaf, the client starts at the root and checks the condition at each node. A subset of the tree represents nodes that have passed a sequence of conditions.

Behavioural

If a pattern is neither creational or structural, it’s a behavioural pattern.

Interpreter: Given a language, defines a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language.

Interpreter Pattern (Source: http://www.mcdonaldland.info/files/designpatterns/designpatternscard.pdf).

Basic Python Implementation

The interpreter pattern reduces the number of total problems by redefining them as combinations of subproblems. As seen below, grammar is represented implicitly by (1) the class hierarchy and (2) the implementations of the interpret methods.

# Core or library code


class Context:
"""Stores variable values for expression evaluation."""

def __init__(self, variables: dict[str, int]):
self.variables = variables

def get_variable_value(self, name: str) -> int:
"""Returns the value of a variable."""
return self.variables.get(name, 0)


class AbstractExpression:
"""Defines the interface for all arithmetic expressions."""

def interpret(self, context: Context) -> int: ...


# Consumer or add-on code


class TerminalVariableExpression(AbstractExpression):
"""Responsible for grammar for variable expression."""

def __init__(self, name: str):
self.name = name

def interpret(self, context: Context) -> int:
return context.get_variable_value(self.name)


class NonTerminalAddExpression(AbstractExpression):
"""Responsible for grammar for addition between two expressions."""

def __init__(self, left: AbstractExpression, right: AbstractExpression):
self.left = left
self.right = right

def interpret(self, context: Context) -> int:
return self.left.interpret(context) + self.right.interpret(context)


# Any component can make code like this (client, core, non-core)


class Client:
def evaluate_expression(
self, expression: AbstractExpression, variables: dict[str, int]
):
"""Evaluates an arithmetic expression with variable values."""
context = Context(variables)
result = expression.interpret(context)
print(f"Result: {result}")
return result


if __name__ == "__main__":
client = Client()

expression_1 = TerminalVariableExpression("x")
result = client.evaluate_expression(expression_1, {"x": 5})
assert result == 5

expression_2 = NonTerminalAddExpression(
TerminalVariableExpression("x"), TerminalVariableExpression("y")
)
result = client.evaluate_expression(expression_2, {"x": 3, "y": 4})
assert result == 7

print("All tests passed!")

Targeted Problem: solve different combinations of problems that may be represented as sentences in a language.

Non-target problems:

  • The number of problems is small and static.
  • The problems cannot be decomposed into subproblems.

Examples:

  • Backend: Django’s template engine. The HTML file is parsed into nodes with interpret methods. Some nodes are grammatically logical, such as IfNodes and ElseNode. The highest-level interpret method (render) inputs a context object that provides the variables needed for rendering.
  • Frontend: Similarly, React DOM -> HTML DOM. NonterminalExpressions are React Components with children and Terminal Expressions are React Components without children. The conditional logic within components makes the pattern grammatical. The context is the overall state caused by the user’s actions.
  • ML: Decision trees. The terminal nodes are the final decisions and the nonterminal decisions are the earlier conditions.

Chain of Responsibility: Avoids coupling the sender of a request to its receiver by giving more than one object a chance to handle the request.

Chain of responsibility (Source: http://www.mcdonaldland.info/files/designpatterns/designpatternscard.pdf).

Basic Python Implementation

import random
from typing import Optional


# Core or library code


class Handler:
"""Responsible for defining how the handlers can be chained and called"""

def __init__(self, successor: Optional["Handler"] = None):
self.successor = successor

def i_will_handle_request(self, request) -> bool: ...

def handle_request(self, request):
if self.successor:
self.successor.handle_request(request)
else:
print("No successor to handle the request")


# Consumer or add-on code


class ConcreteHandler1(Handler):
"""One way to handle a request"""

def i_will_handle_request(self, request) -> bool:
true_or_false = bool(random.getrandbits(1))
if true_or_false:
print("ConcreteHandler1 will handle the request")
else:
print("ConcreteHandler1 will not handle the request")
return true_or_false

def handle_request(self, request):
if self.i_will_handle_request(request):
print("ConcreteHandler1 handled the request")
else:
super().handle_request(request)


class ConcreteHandler2(Handler):
"""One way to handle a request"""

def i_will_handle_request(self, request) -> bool:
true_or_false = bool(random.getrandbits(1))
if true_or_false:
print("ConcreteHandler2 will handle the request")
else:
print("ConcreteHandler2 will not handle the request")
return true_or_false

def handle_request(self, request):
if self.i_will_handle_request(request):
print("ConcreteHandler2 handled the request")
else:
super().handle_request(request)


# Any component can make code like this (client, core, non-core)


class Client:
def some_method(self, handler: Handler):
"""Does some stuff

- We don't know which handler, if any, will handle the request
"""
handler.handle_request({"type": "A", "msg": "do something"})


if __name__ == "__main__":
# we can chain the handlers however we want, dynamically
handler_chain_a = ConcreteHandler1(ConcreteHandler2())
handler_chain_b = ConcreteHandler2()

print("Handler chain A:")
Client().some_function(handler_chain_a)
print("Handler chain B:")
Client().some_function(handler_chain_b)

Targeted problem: the caller cannot know which singular handler, if any, will handle its request. Instead, a chain of handlers is responsible for determining the singular handler.

Non-target problems:

  • The caller can determine which handler to call, if any. If this is possible, we recommend against using the chain of responsibility. Just determine the handler and call it.
  • The caller wants to call multiple handlers. In this case, there is no need for a chain.

Examples:

  • Frontend: Error-handling in a UI. In most user interfaces, there are multiple areas where errors are displayed. For example, in order of increasing specificity: notifications at the level of the page, at the level of the form, and at the level of the input field. By using the chain of responsibility, the input field does not have to know which object will handle the error.
  • Backend: Organization-specific logic. If some organization-specific logic requires the determination of a handler based on where the request has been, it is appropriate to use a chain of responsibility. As the request goes deeper into the code, candidate handlers can be appended to the chain. Eventually, the chain is used to determine the handling.

Command: Encapsulates a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations. It is the object-oriented version of callbacks.

Command Pattern (Source: http://www.mcdonaldland.info/files/designpatterns/designpatternscard.pdf).

Basic Python Implementation


from enum import Enum

# Core or library code

position = [0, 0]


class Command:
"""Responsible for unifying the interface for calling commands"""

def execute(self): ...


# Consumer or add-on code
class KeysEnum(Enum):
UP = "up"
LEFT = "left"


class ReceiverUp:
"""Responsible for Up behavior"""

def action(self):
print("ReceiverUp action")
position[1] += 1


class ReceiverUpReversal:
"""Responsible for reversing Up behavior"""

def action(self):
print("ReceiverUpReversal action")
position[1] -= 1


class ReceiverLeft:
"""Responsible for Left behavior"""

def action(self):
print("ReceiverLeft action")
position[0] -= 1


class ReceiverLeftReversal:
"""Responsible for reversing Left behavior"""

def action(self):
print("ReceiverLeftReversal action")
position[0] += 1


class ConcreteCommandUp(Command):
"""Responsible for encapsulating some instructions"""

def execute(self):
print("ConcreteCommandUp execute")
ReceiverUp().action()


class ConcreteCommandUpReversal(Command):
"""Responsible for encapsulating some instructions"""

def execute(self):
print("ConcreteCommandUpReversal execute")
ReceiverUpReversal().action()


class ConcreteCommandLeft(Command):
"""Responsible for encapsulating some instructions"""

def execute(self):
print("ConcreteCommandLeft execute")
ReceiverLeft().action()


class ConcreteCommandLeftReversal(Command):
"""Responsible for encapsulating some instructions"""

def execute(self):
print("ConcreteCommandLeftReversal execute")
ReceiverLeftReversal().action()


class Invoker:
"""Responsible for managing the behavior"""

def __init__(
self,
key_to_command: dict[KeysEnum, Command],
key_to_command_reversal: dict[KeysEnum, Command],
):
self.key_to_command = key_to_command
self.key_to_command_reversal = key_to_command_reversal
self.executed_commands = []

def undo(self):
last_executed = self.executed_commands.pop()
self.key_to_command_reversal[last_executed].execute()

def do_something(self, key: KeysEnum):
self.key_to_command[key].execute()
self.executed_commands.append(key)


# Any component can make code like this (client, core, non-core)


class Client:
def some_method(self, invoker: Invoker):
"""Does some stuff
- We don't know who the actors/receivers are,
but we can indirectly invoke them and undo
"""
# start at state = 0
# go to state = 1
invoker.do_something(KeysEnum.UP)
print("Position: ", position)
# go to state = 2
invoker.do_something(KeysEnum.LEFT)
print("Position: ", position)
# undo to state = 1
invoker.undo()
print("Position: ", position)


if __name__ == "__main__":
invoker = Invoker(
{
KeysEnum.UP: ConcreteCommandUp(),
KeysEnum.LEFT: ConcreteCommandLeft(),
},
{
KeysEnum.UP: ConcreteCommandUpReversal(),
KeysEnum.LEFT: ConcreteCommandLeftReversal(),
},
)
Client().some_method(invoker)

Targeted Problem: There is a need to make some behaviour reversible. Usually, there is also an implicit requirement that the memento pattern is too expensive.

Non-target problems:

  • Behaviour does need to be reversible.

Examples:

  • Database: Transactions. DB transactions allow consumers to roll back or commit a sequence of changes. Being able to roll back requires defining the reversal for each change in the sequence. This makes it a command pattern.
  • Frontend: Attaching callbacks to UI widget objects. By attaching behaviour and reversal behaviour to interaction with a UI widget object, the user is able to execute and reverse the behaviour.
  • Backend: Database migration commands. By defining the migration steps in commands, engineers are able to execute the migrations when ready. If a reverse method is provided and commands are stored, this may be used to reverse schema changes in the database. Needing to implement a reverse method makes the command pattern more time-consuming than the memento pattern (restoring from a backup).
  • ML: If you are training an extremely large ML model, restoring the ML model from a checkpoint may be substantially more expensive than reversing some of the steps. In these very rare cases, one should consider using the command pattern.

Memento: Without violating encapsulation, capture and externalize an object’s internal state so that the object can be restored to this state later.

Memento Pattern (Source: http://www.mcdonaldland.info/files/designpatterns/designpatternscard.pdf).

Basic Python Implementation

class Memento:
"""Responsible for holding information about state"""

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


class Originator:
"""Has some state it can afford to duplicate

Responsible for
- some behavior,
- reinitalizing from memento,
- creating a memento
"""

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

def set_memento(self, memento: Memento):
self.my_state = memento.my_state

def create_memento(self):
return Memento(my_state=self.my_state)

def do_something(self):
print("Some originator unique behavior")
self.my_state += 1


class Caretaker:
"""Responsible for taking care of the state and using it to restore the originator

- This may warrant becoming a core class if you have multiple originators
"""

def __init__(self):
self.mementos = []

def restore_to_memento(self, originator: Originator, index: int):
memento = self.mementos[index]
originator.set_memento(memento)

def save_memento(self, originator: Originator) -> Memento:
memento = originator.create_memento()
self.mementos.append(memento)
return memento


def some_function(caretaker: Caretaker):
originator = Originator(my_state=0)
originator.do_something()
caretaker.save_memento(originator)
print("state after first and only save:", originator.my_state)
# some time passes
originator.do_something()
originator.do_something()
originator.do_something()
originator.do_something()
print("state before restore:", originator.my_state)
# restore it to last saved state (state 1)
caretaker.restore_to_memento(originator, -1)
print("state after restore:", originator.my_state)


if __name__ == "__main__":
caretaker = Caretaker()
some_function(caretaker)

Targeted Problem: Being able to restore to a state (without implementing behaviour-specific undo logic such as in the command pattern).

Non-target problems:

  • The snapshot is expensive to create/store/load. Consider a command pattern instead.
  • The code for restoring the state is simple. Consider a command pattern instead.

Examples:

  • Core: How Git works. Git makes a snapshot of a file for every commit in which the file changes. A snapshot is then referenced, in some cases indirectly, by all the commits in which the file did not change. In doing this, Git allows you to traverse the entire project history. Cool, right?
  • Database: Database snapshots. By keeping a sequence of snapshots, the caretaker can restore the database to any point in the sequence.
  • Backend: We can’t think of an example. Most modern backends are stateless. There are db snapshots for backups, but that’s a point for databases. For editing workflows, they use the command pattern. Let me know in the comments if you can think of one and I’ll credit you here.
  • Frontend: Similarly to backends, frontends use the db for snapshots of the command pattern for editing workflows.
  • ML: PyTorch and TensorFlow model checkpoints. The checkpoint is a memento. Each framework has its own methods for creating and loading from a model checkpoint. Without checkpoints, failures in long-running training are painful.

Iterator: Given a complicated data structure, provide a simple way to iterate through its elements.

Iterator Pattern (Source: http://www.mcdonaldland.info/files/designpatterns/designpatternscard.pdf).

Basic Python Implementation

Although this is Python is basic, it is more common to use a generator.

# Core or library code
class Iterator:
"""Responsible for defining how to iterate through a list"""

def next(self) -> str | int: ...


class Aggregate:
"""Responsible for defining how to create an interator and get data"""

def create_iterator(self) -> Iterator: ...

def get(self, ptr: str | int): ...


# Consumer or add-on code


class ConcreteIterator(Iterator):
"""Responsible for keeping track of the iteration state"""

def __init__(self):
self.ptr = 0

def next(self):
res = self.ptr
self.ptr += 1
return res


class ConcreteAggregate(Aggregate):
"""Responsible for holding and allowing access to some data"""

def __init__(self):
self.data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

def get(self, ptr: int):
return self.data[ptr]

def create_iterator(self) -> Iterator:
return ConcreteIterator()


# Any component can make code like this (client, core, non-core)


class Client:
def some_method(self, aggregate: Aggregate):
"""Does some stuff

- We don't know where the data is or how it's organized,
but we know how to iterate through it
- We can also have multiple iteration states
"""
iterator_1 = aggregate.create_iterator()
iterator_2 = aggregate.create_iterator()
data_0 = aggregate.get(iterator_1.next())
print("data_0 from iterator_1", data_0)
data_1 = aggregate.get(iterator_1.next())
print("data_1 from iterator_1", data_1)
data_0_from_iterator_2 = aggregate.get(iterator_2.next())
print("data_0_from_iterator_2", data_0_from_iterator_2)


if __name__ == "__main__":
Client().some_method(ConcreteAggregate())

Targeted Problem: There is a complicated data structure that would benefit from iterative access.

Non-target problems:

  • The data structure is not complicated. Convert to an array then use your language’s core iteration interface.

Examples:

  • Backend: Iterating through a complicated data structure. For example, you may need to iterate over a binary search tree in increasing order. This can be addressed by creating an iterator that is responsible for knowing how to iterate in increasing order and keeping track of the iteration state.
  • Frontend: Paginating through a REST API list. API list endpoints often limit responses to 25 or 50 at a time. To request previous or subsequent pages, the iterator pattern is often applied in a page query parameter.
  • ML: DataLoaders in PyTorch. DataLoaders in PyTorch allow consumers to iterate over batches of data in a Dataset. This abstracts away the complicated structure of the dataset.

Observer: Defines a one-to-many dependency between objects so that when one object changes state all its dependents are notified and updated automatically

Observer Pattern (Source: http://www.mcdonaldland.info/files/designpatterns/designpatternscard.pdf)

Basic Python Implementation

# Core or library code


class Observer:
"""Responsible for defining how to respond to notifications"""

def update(self, message: str): ...


class Subject:
"""Responsible for defining how take in subscribers, remove subscribers, and notify them of events"""

def attach(self, observer: Observer): ...

def detach(self, observer: Observer): ...

def notify(self): ...


# Consumer or add-on code


class ConcreteSubject(Subject):
"""Responsible for taking in subscribers, removing subscribers, and notifying them of events"""

def __init__(self):
self.observers = []
self.subject_state = "something"

def attach(self, observer: Observer):
self.observers.append(observer)

def detach(self, observer: Observer):
idx = self.observers.index(observer)
self.observers.pop(idx)

def notify(self, message: str):
for observer in self.observers:
observer.update(message)

def change_state(self, message: str):
self.subject_state = message
self.notify(message)


class ConcreteObserver(Observer):
"""Responsible for responding to notifications"""

def __init__(self, id: int, observer_state: str):
self.id = id
self.observer_state = observer_state

def update(self, message: str):
self.observer_state += " " + message
print(f"Observer {self.id} state updated to: {self.observer_state}")


# Any component can make code like this (client, core, non-core)


def some_function(subject: ConcreteSubject, observers: list[ConcreteObserver]):
"""Does some stuff"""
for observer in observers:
subject.attach(observer)
# Change state (and notify observers)
print("Changing subject state to A")
subject.change_state("A")
print("Detaching observer 1")
subject.detach(observers[0])
print("Changing subject state to B")
subject.change_state("B")


if __name__ == "__main__":
subject = ConcreteSubject()
observer_1 = ConcreteObserver(1, "1 -")
observer_2 = ConcreteObserver(2, "2 -")
some_function(subject, [observer_1, observer_2])

Targeted Problem: There are events for which many objects, as determined during runtime, need to react.

Non-target problems:

  • The objects that need to react are known statically. Consider making a list and calling them directly.
  • The objects are not responsible for determining how they need to react. Consider the mediator pattern below.

Examples:

  • Backend: Chat rooms. Observers in the chat room subscribe to the chat group. In chat rooms, the observers also send messages to the group (equivalent to calling subject.change_state). The group then calls all the observers to update them about the new state.
  • Frontend: Web 2.0 would not be possible without the observer pattern. By converting user actions to events that can be subscribed by elements in the native DOM (element.addListener), it became possible for users to interact with websites.
  • Systems: Publisher-subscribers system, also known as pub-sub. In a pub-sub, services send a message to a subject topic that can be subscribed by multiple observing servers. In response to messages, the subscribers can trigger their behaviours. See the ML example below.
  • ML: Model monitoring. Although model monitoring is more of a systems example, it is core to successful ML development. Briefly, ML performance metrics are published to a topic for which subscribers can react by notifying stakeholders, scheduling retraining, and modifying deployment.

Mediator: Defines an object that encapsulates how a set of objects interact. It’s a fine line, but this is technically not a structural pattern because it defines the interaction.

Mediator Pattern (Source: http://www.mcdonaldland.info/files/designpatterns/designpatternscard.pdf).

Basic Python Implementation

# Core or library code
class Colleague:
"""Responsible for unifying communication with mediator"""

def inform(self, mediator: "Mediator", what_i_did: str): ...


class Mediator:
"""Responsible for unifying behavior coordination"""

def receive(self, colleague: Colleague, what_colleague_did: str): ...


# Consumer or add-on code


def is_colleague_a(colleague: Colleague):
return colleague.__class__.__name__ == "ConcreteColleagueA"


def is_colleague_b(colleague: Colleague):
return colleague.__class__.__name__ == "ConcreteColleagueB"


class ConcreteMediator(Mediator):
"""Responsible for coordinating behavior"""

def __init__(self, colleagues: list[tuple[str, "Colleague"]]):
self.colleagues = {key: c for key, c in colleagues}

def receive(self, colleague: Colleague, what_colleague_did: str):
if is_colleague_a(colleague) and what_colleague_did == "give gift":
colleague_b = self.colleagues["colleague_b"]
assert isinstance(colleague_b, ConcreteColleagueB)
colleague_b.say_thank_you(self)
elif is_colleague_b(colleague) and what_colleague_did == "say thank you":
print("Nothing to do here")


class ConcreteColleagueA(Colleague):
"""Responsible for some behavior"""

def give_gift(self, mediator: "Mediator"):
print("gave a gift")
self.inform(mediator, "give gift")

def inform(self, mediator: "Mediator", what_i_did: str):
mediator.receive(self, what_i_did)


class ConcreteColleagueB(Colleague):
"""Responsible for some behavior"""

def say_thank_you(self, mediator: "Mediator"):
print("said thank you")
self.inform(mediator, "say thank you")

def inform(self, mediator: "Mediator", what_i_did: str):
mediator.receive(self, what_i_did)


# Any component can make code like this (client, core, non-core)


class Client:
def some_function(
self,
concrete_colleague_a: ConcreteColleagueA,
concrete_colleague_b: ConcreteColleagueB,
mediator: Mediator,
):
"""Calls the colleague's methods

- We don't know the colleague's colleague or mediator,
but we don't have to worry about coordinating the behavior
"""
print("Calling concrete_colleague_a.give_gift(mediator)")
concrete_colleague_a.give_gift(mediator)
print("Calling concrete_colleague_b.say_thank_you(mediator)")
concrete_colleague_b.say_thank_you(mediator)


if __name__ == "__main__":
client = Client()
concrete_colleague_a = ConcreteColleagueA()
concrete_colleague_b = ConcreteColleagueB()
mediator = ConcreteMediator(
[("colleague_a", concrete_colleague_a),
("colleague_b", concrete_colleague_b)]
)
client.some_function(concrete_colleague_a, concrete_colleague_b, mediator)

Targeted Problem: Avoid coupling interactions between colleagues.

Non-target problems:

  • Colleagues can directly interact — they do not need to be decoupled.
  • Colleagues are responsible for determining how they respond to one another. For that, consider the observer pattern.

Examples:

  • Backend: Django submodules and signals. In Django, an application can be made up of several submodules. If they imported one another, you end up with coupling and potentially even circular import issues. Alternatively, they can interact indirectly via Django signals. All the submodules send signals to the Django signalling system, which then executes your signals.
  • Frontend: Mutually exclusive filter manager. If a user toggles one filter, the manager ensures to toggle off the other filters. By putting control in the manager, individual filters do not have to understand how to respond to their context.
  • ML: Hyperparameter tuning space exploration. When using multiple strategies for tuning the hyperparameters for an ML model, it is common to have each strategy report its results to a mediator. The mediator then decides how to adjust every strategy.

State: Allows an object to alter its behavior when its internal state changes.

State pattern (Source: http://www.mcdonaldland.info/files/designpatterns/designpatternscard.pdf).

Basic Python Implementation

# Core or library code


class State:
"""Responsible for unifying the state call"""

def handle(self, event): ...


# Consumer or add-on code
class ConcreteState1(State):
"""Responsible for handling events in a specific way"""

def handle(self, event):
print("Handling event in ConcreteState1")


class ConcreteState2(State):
"""Responsible for handling events in a different specific way"""

def handle(self, event):
print("Handling event in ConcreteState2")


class Context:
"""Responsible for determining which concrete state class gets called"""

def __init__(self, state_to_handler: dict[str, State]):
self.state = "basic_state"
self.state_to_handler = state_to_handler

def request(self, event):
self.state_to_handler[self.state].handle(event)


# Any component can make code like this (client, core, non-core)


def some_function(event: str, context: Context):
"""Does some stuff

- We don't know what handlers are in the context, but we know that
its state will determine how it handles the request
"""
print(f"Sending event {event} in state {context.state}")
context.request(event)
# change the behavior of the context
context.state = "advanced_state"
print(f"Sending event {event} in state {context.state}")
context.request(event)


if __name__ == "__main__":
state_to_handler = {
"basic_state": ConcreteState1(),
"advanced_state": ConcreteState2(),
}
context = Context(state_to_handler)
some_function("some_event", context)

Targeted Problem: An object is responsible for choosing one of several separate-able state-based behaviours.

Non-target problems:

  • State does not substantially affect the behaviour of the responsible class. Though in this case, it may be reasonable to make a lower-level class set for the affected responsibility and then apply the state pattern.
  • Behaviour is determined by an upstream component. For that, consider the strategy pattern below.

Examples:

  • Backend: Payment-tier-dependent behaviour. In a lot of applications, different payment tiers offer different behaviour. In this case, the payment state retrieved from the database can be used to determine which payment-based behaviour to use.
  • Frontend: A React component that is responsible for rendering one of several components. The choice of a component depends on its state.
  • ML: Model training phasing. ML training can be split into multiple phases through which it cycles such as training, validation, and testing. Instead of checking the state in each step, you can make the code more modular by separating state-based behaviour into different classes and then deciding which class to use based on the state. This might be overkill if phase-based behaviour is not drastically different.

Strategy: Defines a family of algorithms, encapsulates each one, and makes them interchangeable.

Strategy Pattern (Source: http://www.mcdonaldland.info/files/designpatterns/designpatternscard.pdf).

Basic Python Implementation

# Core or library


class Strategy:
"""Responsible for unifying how a behavior is called"""

def execute(self): ...


# Consumer or add-on code


class ConcreteStrategyA(Strategy):
"""Responsible for encapsulating a behavior"""

def execute(self):
print("Executing strategy A")


class ConcreteStrategyB(Strategy):
"""Responsible for encapsulating another behavior"""

def execute(self):
print("Executing strategy B")


# Any component can make code like this (client, core, non-core)


class Context:
"""Responsible for some behavior that can be different based on strategy"""

def __init__(self, strategy: Strategy):
self.strategy = strategy

def do_something(self):
print("Context is doing something")
print("Context is going to use strategy")
self.strategy.execute()


def some_function(strategy: Strategy):
"""Does some stuff

- We don't know how the strategy works, but we know the context can use
it
"""
context = Context(strategy)
context.do_something()


if __name__ == "__main__":
print("Using strategy A")
some_function(ConcreteStrategyA())
print("Using strategy B")
some_function(ConcreteStrategyB())

Targeted Problem: A behaviour can vary enough that it needs a separate class family, and the consumer needs to be responsible for determining the behaviour.

Non-target problems:

  • Behaviour is not variable.
  • Behaviour needs to be determined not by the consumer. For that, consider the state pattern above.

Examples:

  • Backend: Platforms that accept server-side code from the client. Some platforms such as Leetcode accept code from the client. In that case, the submitted code is the strategy. The backend executes the strategy.
  • Frontend: Passing callbacks as props in React. Instead of the component determining its own behaviour based on prop or state, it executes the strategy of the callback prop.
  • ML: Minimizer classes. Minimization behaviour is usually separated into different minimizer classes for minimizing an objective function in the process of optimizing an ML model.

Template Method: Defines the skeleton of an algorithm in an operation, deferring some steps to subclasses.

Template Method (Source: http://www.mcdonaldland.info/files/designpatterns/designpatternscard.pdf).

Basic Python Implementation

class AbstractClass:
"""Responsible for enforcing implemention of sub_method and using it"""

def sub_method(self):
raise NotImplementedError("Sub method must be implemented")

def template_method(self):
print("AbstractClass is doing something common")
self.sub_method()
print("AbstractClass is doing something common")


class ConcreteClass(AbstractClass):
"""Responsible for implementing the sub_method"""

def sub_method(self):
print("ConcreteClass is doing something varying")


def some_function(obj: AbstractClass):
"""Does some stuff

- We know how template_method is implemented,
but we don't know what sub_method will be called
"""
obj.template_method()


if __name__ == "__main__":
some_function(ConcreteClass())

Targeted Problem: A behaviour can vary enough that it delegates implementation to subclasses. In contrast to strategy and state patterns, the template method pattern uses inheritance to vary behaviour.

Non-target problems:

  • There is no consistent ‘recipe’ or template in which the behaviour can be injected.

Examples:

  • Backend: The strategy pattern above cannot be replaced with a template method pattern, but the state pattern above can be replaced with a template method pattern. Instead of letting a class decide its behaviour based on state, dedicated subclasses can be implemented and called based on state.
  • Frontend: We don’t know about every framework, but React encourages varying behaviour by composition. As a result, we cannot name a modern realistic use case for the template method pattern.
  • ML: Similarly to the strategy pattern, minimization behaviour can be varied by inserting from an optimizer class that requires the implementation of a minimization step.

Visitor: Represents an operation to be performed on the elements of an object structure.

Visitor Pattern (Source: http://www.mcdonaldland.info/files/designpatterns/designpatternscard.pdf).

Basic Python Implementation

# Core or library code


class Element:
"""Responsible for defining how to welcome a visitor"""

def accept(self, v: "Visitor"): ...


class Visitor:
"""Responsible for enforcing visiting of all elements"""

def visit_element_a(self, a: "ConcreteElementA"): ...

def visit_element_b(self, b: "ConcreteElementB"): ...


# Consumer or add-on code
class ConcreteElementA(Element):
"""Responsible for welcoming a visitor"""

def accept(self, v: "Visitor"):
print("ConcreteElementA is welcoming the visitor")
v.visit_element_a(self)


class ConcreteElementB(Element):
"""Responsible for welcoming a visitor"""

def accept(self, v: "Visitor"):
print("ConcreteElementB is welcoming the visitor")
v.visit_element_b(self)


# Any component can make code like this (client, core, non-core)


class ConcreteVisitor(Visitor):
"""Responsible for integrating its behavior with the concrete elements"""

def visit_element_a(self, a: ConcreteElementA):
print("ConcreteVisitor is integrating its behavior with ConcreteElementA")

def visit_element_b(self, b: ConcreteElementB):
print("ConcreteVisitor is integrating its behavior with ConcreteElementB")


def some_function(visitor: Visitor, elements: list[Element]):
"""Does some stuff

- We don't know the visitor or the elements, but we know the visitor
can integrate its behavior with the elements
"""
for el in elements:
el.accept(visitor)


if __name__ == "__main__":
some_function(ConcreteVisitor(), [ConcreteElementA(), ConcreteElementB()])

Targeted Problem: There are behaviours that involve interacting with different elements of an object, but the behaviour is not worth modifying the object’s code.

Non-target problems:

  • The visited object’s attributes are not necessary for determining the visitor’s behaviour.
  • The behaviour is core and therefore worth modifying the object’s code.

Examples:

  • Backend: Writing graph to disk. Using a serializer class that works with objects in a graph to write the graph onto a disk. This should not be the responsibility of the graph objects.
  • Frontend: Theming the component tree. Instead of making every component be responsible for determining how to behave based on a specific theme, a theming object that determines the theme can be passed into the context and accepted by every component.
  • ML: Feature extraction from unstructured data. It is common to implement classes for unstructured data such as text, video, and audio. It should not be their responsibility though to own the code for every feature extraction. Instead, they can accept a feature extraction visitor.

Conclusion

We just wanted to share the quiz and the study examples above! We hope you enjoy it. Let us know if you have any feedback. Thanks for reading.

If you are a startup looking for software product development expertise, check us out at abahope.com

URL: https://www.abahope.com/design_patterns_quiz

--

--

Nassir Al-Khishman

My passion is optimizing Python backends and ML infrastructure. I am a software engineer at abahope.com