Design Patterns in Python

Notes about Design Patterns in Python


Last Updated: November 10, 2020 by Pepe Sandoval



Want to show support?

If you find the information in this page useful and want to show your support, you can make a donation

Use PayPal

This will help me create more stuff and fix the existent content...


Design Patterns in Python

  • Design patterns are common architectural approaches that people have observed as they've been designing object oriented software and people decided to make a catalog out of the most common ones

  • The opposites of pattern is an anti-pattern

Index: Design Patterns in Python

Gamma Categorization: Types of design patterns

SOLID Design Principles

  1. Single Responsibility Principle (SRP) / Separation of Concerns (SoC)
  2. Open-Closed Principle (OCP)
  3. Liskov Substitution Principle (LSP)
  4. Interface Segregation Principle (ISP)
  5. Dependency Inversion Principle (DIP)

Patterns

  1. Builder

    • It is a separate component for when object construction gets complicated and you need to make several calls to initialize the object
    • You can have mutually cooperating sub-builders
    • Often have fluent interface (return self) to chain several calls
  2. Factories

    • Used to make object creation more expressive and explicit
    • Can be standalone class or inner class
  3. Prototype

    • Create an object from an existing object
    • Requires deep.copy()
    • Make some prototype objects, copy them then provide a factory to customize those
  4. Singleton

    • Used when you need to ensure just a single instance exists
    • Implemented with decorator or metaclass
  5. Adapter

    • Helps convert the interface you get to the interface you need
  6. Bridge

    • Separates abstraction from implementation
  7. Composite

    • Allows us to treat individual objects and compositions (groups) of objects uniformly
  8. Decorator

    • Attach additional responsibilities or functionality to objects without necessarily modifying those objects or inherit from them
  9. Façade

    • Provide a single unified interface over a set of interfaces
    • Try to provide a friendly and easy-to-use interface
  10. Flyweight

    • Efficiency technique to support large number of similar objects without actually storing redundant data
  11. Proxy

    • Provide an intermediate object that forwards calls to the real object while providing additional functions
  12. Chain of Responsibility

    • Allows us to process information/events in a chain one after another
    • Each element can refer to the next element or have a list to go through and notify each element on the list
  1. Command

    • Encapsulates a request into a separate object
    • Good for record keeping, replay, undo/redo, etc
  2. Interpreter

    • Used to transform text input into data structures
    • Used in Compiler Theory to implement compilers, analysis and interpretation tools, etc
  3. Iterator

    • Provides an interface for accessing elements on an object
  4. Mediator

    • Provides mediator/communication services between two or more objects without necessarily being aware of each other
    • E.g: a chat room where users join/exit and interact with each other
  5. Memento

    • Returns tokens representing system states
    • Memento is a snapshot of the system that can be used to go back to that state
  6. Observer

    • Allows notifications of changes/happenings in a component so other components can subscribe to the observer and get notifications when an event happens
  7. State

    • Used to model systems by having a set of possible states and describing the transitions between those states
  8. Strategy

    • Define a skeleton algorithm with details filled by the implementor (strategy) which is an object that has certain interface implements for a specific case
    • Uses ordinary composition by taking an object(s) as parameter(s) of the constructor
  9. Template Method

    • Define a base class with a template method and abstract members, the algorithm uses those abstract members, you inherit from the base class, provide actual implementations for those abstract members and call the template method
    • Uses inheritance
  10. Visitor

    • Allows addition of functionality to class hierarchies
    • Allows us to traverse complicated data structures

Gamma Categorization: Types of design patterns

Types of design patterns

Creational Patterns

  • Patterns that deal with the construction or creation of objects
    • Deal with explicit (constructor call) and implicit (Not calling the constructor directly for example when usingDependency Injection, Reflection, etc) construction of objects
  • These pattern also handle the initialization of an object which can be:
    • wholesale: single statement like in a constructor call
    • piecewise: step-by- where requires multiple steps to init an object

Structural Patterns

  • Patterns concerned with the structure of the classes. Concerned with

    • Class members
    • Whether the class adheres to certain interface
    • Good API design
  • These type of patterns are usually wrapper patterns so they are all about the idea of making the interface has convenient to use as possible (Make objects usable and makes API is usable for other people)

Behavioral Patterns

  • Behavior patterns don't really follow any central theme.
  • These type of patterns solve a particular problem and they solve it in a particular way. Each have a particular set of concerns

SOLID Design Principles

  • The SOLID Design Principles are a set of 5 selected design principles related to software design

SOLID Design Principles

Single Responsibility Principle (SRP) / Separation of Concerns (SoC)

  • If you have a class that class should have primary responsibility and should not take on other responsibilities.

  • Do NOT overload your classes with too many responsibilities

  • For example adding the responsibility of storing its data in a persistent way (the responsibility of persistence) to a class that is supposed to be an abstraction for a user, a student, a journal, etc. It would be better to have a PersitanceManager which will be the one responsible of saving an object to a file

  • It enforces this idea that a class should have a single reason to change and that change should be somehow related to its primary responsibility.

  • The god object is an anti-pattern where you add all the functionality in a class ending up with a massive class

  • Different classes should handle different independent tasks/problems

The Open-Closed Principle (OCP)

  • OCP = open for extension but closed for modification

  • In a strict sense the OCP suggests that when you add new functionality you add ONLY via extension not via modification

  • At the implementation level the OCP suggest that after you've written and tested a particular class or code you should not modify it instead you should extend it.

  • If we continue to modify a class adding and adding functionality we could cause what is called state space explosion meaning we have a class with too many methods or functionality

The specification pattern

class Specification:
    def is_satisfied(self, item):
        pass
    # and operator makes life easier
    def __and__(self, other):
        return AndSpecification(self, other)

class Filter:
    def filter(self, items, spec):
        pass

class ColorSpecification(Specification):
    def __init__(self, color):
        self.color = color
    def is_satisfied(self, item):
        return item.color == self.color

class SizeSpecification(Specification):
    def __init__(self, size):
        self.size = size
    def is_satisfied(self, item):
        return item.size == self.size

class AndSpecification(Specification):
    def __init__(self, *args):
        self.args = args
    def is_satisfied(self, item):
        return all(map(
            lambda spec: spec.is_satisfied(item), self.args))

class BetterFilter(Filter):
    def filter(self, items, spec):
        for item in items:
            if spec.is_satisfied(item):
                yield item


apple = Product('Apple', Color.GREEN, Size.SMALL)
tree = Product('Tree', Color.GREEN, Size.LARGE)
house = Product('House', Color.BLUE, Size.LARGE)
products = [apple, tree, house]

bf = BetterFilter()

print('Green products (new):')
green = ColorSpecification(Color.GREEN)
for p in bf.filter(products, green):
    print(f' - {p.name} is green')

print('Large products:')
large = SizeSpecification(Size.LARGE)
for p in bf.filter(products, large):
    print(f' - {p.name} is large')

print('Large blue items:')
""" large_blue = AndSpecification(large, ColorSpecification(Color.BLUE)) """
large_blue = large & ColorSpecification(Color.BLUE)
for p in bf.filter(products, large_blue):
    print(f' - {p.name} is large and blue')

Liskov Substitution Principle (LSP)

  • LSP states that if you have some interface that takes some sort of base class you should be able to stick a derived class in there and everything should work.

  • When you have a function or interface that takes as input a base class you should be able to stick in any of its inheritor. e.g. if you have a move() function that takes as input any object of the class Animal if you put there any object that inherits from Animal it should work, like Lion, Tiger, Dog, etc.

  • you should be able to substitute a base type for a derivative type

Interface Segregation Principle (ISP)

  • The ISP basically says" Don't put too much into an interface, split it into separate interfaces

  • We don't want to put to much into a single interface because YAGNI (You aren't gonna need it)

  • The idea is that you don't really want to stick to many elements (too many methods) into an interface.

  • Making interfaces which feature too many elements is not a good idea because you're forcing your clients to define methods which they might not even need.

  • Instead of having one large interface with several members/methods you have separate interfaces that people can implement. Split them into the smallest interfaces you can build so that people don't have to implement more than they need to.

class Printer:
    @abstractmethod
    def print(self, document): pass

class Scanner:
    @abstractmethod
    def scan(self, document): pass

class MyPrinter(Printer):
    def print(self, document):
        print(document)

class Photocopier(Printer, Scanner):
    def print(self, document):
        print(document)

    def scan(self, document):
        pass  # something meaningful

class MultiFunctionDevice(Printer, Scanner):  # , Fax, etc
    @abstractmethod
    def print(self, document):
        pass

    @abstractmethod
    def scan(self, document):
        pass

class MultiFunctionMachine(MultiFunctionDevice):
    def __init__(self, printer, scanner):
        self.printer = printer
        self.scanner = scanner

    def print(self, document):
        self.printer.print(document)

    def scan(self, document):
        self.scanner.scan(document)

Dependency Inversion Principle (DIP)

  • DIP states that high-level modules should not depend upon low-level ones, use abstractions instead

  • In other words High level classes or high level modules in your code should not use directly low level modules instead they should depend on abstractions (Abstract class or class with abstract methods)

  • Essentially you just want to depend on interfaces so you can swap them at the low level without needing to change high-level module's code

A high-level module is a modules that uses others modules (low-level ones) which can be much closer to the hardware

Builder

Single Builder

  • Builder definition: When piecewise object construction is complicated provide an API for doing it succinctly (precisely and clearly)

  • The Builder pattern is used when we need to create object that requires a lot steps to initialize or a lot of arguments because it can be configured in many ways

  • Attempts to breakdown the whole initialization of an object into methods of a special component called the Builder

  • The Builder provides an API (set of functions or methods) for constructing an object in a clear and explicit way

  • This patterns tries to make the implementation of the construction of an object a piecewise construction instead of a having a massive call to a initializer that does everything

  • Required when you have several processes needed to construct something

  • Usually a builder will at some point call the constructor of another class to build/create an object and also store a reference to this object so the operations are perform through the builder even if they are defined in another class

    • The builder will need to have a relation with the class that it creates, so usually the class provides a method that returns a builder
class HtmlElement:
    indent_size = 2
    def __init__(self, name="", text=""):
        self.name = name
        self.text = text
        self.elements = []
    def __str__(self, indent):
        lines = []
        i = ' ' * (indent * self.indent_size)
        lines.append(f'{i}<{self.name}>')
        if self.text:
            i1 = ' ' * ((indent + 1) * self.indent_size)
            lines.append(f'{i1}{self.text}')
        for e in self.elements:
            lines.append(e.__str(indent + 1))
        lines.append(f'{i}')
        return '\n'.join(lines)
    def __str__(self):
        return self.__str(0)
    @staticmethod
    def create(name):
        return HtmlBuilder(name)

class HtmlBuilder:
    __root = HtmlElement()
    def __init__(self, root_name):
        self.root_name = root_name
        self.__root.name = root_name
    def add_child(self, child_name, child_text): # not fluent
        self.__root.elements.append(
            HtmlElement(child_name, child_text)
        )
    def add_child_fluent(self, child_name, child_text):  # fluent
        self.__root.elements.append(
            HtmlElement(child_name, child_text)
        )
        return self
    def clear(self):
        self.__root = HtmlElement(name=self.root_name)
    def __str__(self):
        return str(self.__root)

""" ordinary non-fluent builder """
""" builder = HtmlBuilder('ul') """
builder = HtmlElement.create('ul')
builder.add_child('li', 'hello')
builder.add_child('li', 'world')
print('Ordinary builder:')
print(builder)

""" fluent builder """
builder.clear()
builder.add_child_fluent('li', 'hello').add_child_fluent('li', 'world')
print('Fluent builder:')
print(builder)

Multiple Builders / Builder Facets

  • If you have an object that is very complicated to build you may need more than one builder or in other words multiple builders

  • For a multiple builders implementation we usually have a base builder that creates and stores an empty object that can be subsequently build up or in other words initialized

    • The empty object allows sub-builder to work with an object that is already in memory
class Person:
    def __init__(self):
        print('Creating an instance of Person')
        # address
        self.street_address = None
        self.postcode = None
        self.city = None
        # employment info
        self.company_name = None
        self.position = None
        self.annual_income = None
    def __str__(self) -> str:
        ret  = f'Address: {self.street_address}, '
        ret += f'{self.postcode}, '
        ret += f'{self.city}\n'
        ret += f'Employed at {self.company_name} '
        ret += f'as a {self.position} '
        ret += f'earning {self.annual_income}'
        return ret

class PersonBuilder:  # facade
    def __init__(self, person=None):
        if person is None:
            self.person = Person()
        else:
            self.person = person
    @property
    def lives(self):
        return PersonAddressBuilder(self.person)
    @property
    def works(self):
        return PersonJobBuilder(self.person)
    def build(self):
        return self.person

class PersonJobBuilder(PersonBuilder):
    def __init__(self, person):
        super().__init__(person)
    def at(self, company_name):
        self.person.company_name = company_name
        return self
    def as_a(self, position):
        self.person.position = position
        return self
    def earning(self, annual_income):
        self.person.annual_income = annual_income
        return self

class PersonAddressBuilder(PersonBuilder):
    def __init__(self, person):
        super().__init__(person)
    def at(self, street_address):
        self.person.street_address = street_address
        return self
    def with_postcode(self, postcode):
        self.person.postcode = postcode
        return self
    def in_city(self, city):
        self.person.city = city
        return self

if __name__ == '__main__':
    pb = PersonBuilder()
    p = pb\
        .lives\
            .at('123 London Road')\
            .in_city('London')\
            .with_postcode('SW12BC')\
        .works\
            .at('Fabrikam')\
            .as_a('Engineer')\
            .earning(123000)\
        .build()
    print(p)
    p2 = PersonBuilder()\
          .lives\
              .at('1234 Coco')\
              .in_city('Guadalajara')\
              .with_postcode('44865')\
          .works.at('FSL MX')\
              .as_a('Engineer')\
              .earning(10000)\
          .build()
    print(p2)

Multiple Builder with inheritance

  • For an implementation with inheritance the idea is that we define a new class for each new builder
class Person:
    def __init__(self):
        self.name = None
        self.position = None
        self.date_of_birth = None
    def __str__(self):
        return f'{self.name} born on {self.date_of_birth} works as a {self.position}'

class PersonBuilder:
    def __init__(self):
        self.person = Person()

    def build(self):
        return self.person

class PersonInfoBuilder(PersonBuilder):
    def called(self, name):
        self.person.name = name
        return self

class PersonJobBuilder(PersonInfoBuilder):
    def works_as_a(self, position):
        self.person.position = position
        return self

class PersonBirthDateBuilder(PersonJobBuilder):
    def born(self, date_of_birth):
        self.person.date_of_birth = date_of_birth
        return self

if __name__ == '__main__':
    pb = PersonBirthDateBuilder()
    me = pb\
        .called('Pepe')\
        .works_as_a('coder')\
        .born('1/1/1991')\
        .build()  # this does NOT work in C#/C++/Java/...
    print(me)

Factories

  • Have the intention to make initializer/constructor descriptive, explicit and avoid optional parameter hell

  • A Factory is a component responsible solely for the complete creation of objects (not necessarily its initialization)

  • Types of factory implementation:

    • Factory Method: Usually an static method called to create an object
    • Factory: Separate class in charge of creating different types of objects that are related to each other
    • Abstract Factory: which is just class hierarchy of factories used to create related objects

Factory Method

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __str__(self):
        return f'x: {self.x}, y: {self.y}'
    @staticmethod
    def new_cartesian_point(x, y):
        return Point(x, y)
    @staticmethod
    def new_polar_point(rho, theta):
        return Point(rho * sin(theta), rho * cos(theta))

Factory / Factory Class

# take out factory methods to a separate class
class PointFactory:
    @staticmethod
    def new_cartesian_point(x, y):
        return Point(x, y)
    @staticmethod
    def new_polar_point(rho, theta):
        return Point(rho * sin(theta), rho * cos(theta))

Abstract Factory / Factory Base

  • If you have a hierarchy of types then you can have a corresponding hierarchy of factories and so at some point you would have an Abstract Factory as a base class of other factories.
from abc import ABC
from enum import Enum, auto

class HotDrink(ABC):
    def consume(self):
        pass

class Tea(HotDrink):
    def consume(self):
        print('This tea is nice but I\'d prefer it with milk')

class Coffee(HotDrink):
    def consume(self):
        print('This coffee is delicious')

class HotDrinkFactory(ABC):
    def prepare(self, amount):
        pass

class TeaFactory(HotDrinkFactory):
    def prepare(self, amount):
        print(f'Put in tea bag, boil water, pour {amount}ml, enjoy!')
        return Tea()

class CoffeeFactory(HotDrinkFactory):
    def prepare(self, amount):
        print(f'Grind some beans, boil water, pour {amount}ml, enjoy!')
        return Coffee()

class HotDrinkMachine:
    factories = {}
    initialized = False

    # being Strict this violates OCP but
    # we only need to change this enum for extension
    class AvailableDrink(Enum):
        COFFEE = auto()
        TEA = auto()

    def __init__(self):
        if not self.initialized:
            self.initialized = True
            for d in self.AvailableDrink:
                name = d.name[0] + d.name[1:].lower()
                factory_name = name + 'Factory'
                factory_instance = eval(factory_name)()
                self.factories[name.lower()] = factory_instance
    def make_drink(self):
        print('Available drinks:')
        for f in self.factories:
            print(f)
        name = str(input(f'Please pick drink name: ')).lower()
        amount = int(input(f'Specify amount: '))
        return self.factories[name].prepare(amount)

if __name__ == '__main__':
    hdm = HotDrinkMachine()
    drink = hdm.make_drink()
    drink.consume()

Prototype

  • Comes from the idea that we don't start things from scratch we instead copy and modify or in other words we re-iterate existing designs, we look at what other people have already done and try to improve upon the existing constructs

  • It is a pattern that is used when there is an already existent design (can be partially or fully constructed) that you want to modify, extend, customize or complete

  • A Prototype is a partially or fully initialized object that you copy (clone) and make use of

Prototype Implementation

  • Prototype pattern requires to do deep copy of objects copy.deepcopy()

  • You create a partially constructed object, store it somewhere, then deep copy that object, customize the copy and return this resulting instance

    • This process is usually implemented with a Factory
import copy

class Address:
    def __init__(self, street_address, suite, city):
        self.suite = suite
        self.city = city
        self.street_address = street_address
    def __str__(self):
        return f'{self.street_address}, Suite #{self.suite}, {self.city}'

class Employee:
    def __init__(self, name, address):
        self.address = address
        self.name = name
    def __str__(self):
        return f'{self.name} works at {self.address}'

class EmployeeFactory:
    main_office_employee = Employee("", Address("123 East Dr", 0, "London"))
    aux_office_employee = Employee("", Address("123B East Dr", 0, "London"))

    @staticmethod
    def __new_employee(proto, name, suite):
        result = copy.deepcopy(proto)
        result.name = name
        result.address.suite = suite
        return result

    @staticmethod
    def new_main_office_employee(name, suite):
        return EmployeeFactory.__new_employee(
            EmployeeFactory.main_office_employee,
            name, suite
        )

    @staticmethod
    def new_aux_office_employee(name, suite):
        return EmployeeFactory.__new_employee(
            EmployeeFactory.aux_office_employee,
            name, suite
        )

pepe = EmployeeFactory.new_main_office_employee("Pepe", 700)
jane = EmployeeFactory.new_aux_office_employee("Jane", 200)
print(pepe)
print(jane)

Singleton

  • Used for components that only makes sense to have ONLY one in the system and it makes sense to initialize them ONLY once. e.g. DB repository, the initializer call is expensive

  • This pattern requires that the implementation must:

    • Prevent users from initialize the instance more than one time
    • Prevent anyone from creating additional copies
    • Provide a mechanism to do Lazy Instantiation, in other words take care of the instantiation of the singleton because nobody really gets to instantiate the singleton until the singleton is needed for something
  • A Singleton is a component/class which is instantiated only once and everybody who tries to use/access this object basically gets to work with that one instance.

Singleton Implementation with Python decorator

import random

def singleton(class_):
    instances = {}
    def get_instance(*args, **kwargs):
        if class_ not in instances:
            instances[class_] = class_(*args, **kwargs)
        return instances[class_]
    return get_instance

@singleton
class Database:
    def __init__(self):
        print('Loading database. Generated an id of', random.randint(1,101))

if __name__ == '__main__':
    d1 = Database()
    d2 = Database()
    print(d1 == d2)

Singleton Implementation with a Metaclass

import random

class Singleton(type):
    """ Metaclass that creates a Singleton base type when called. """
    _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 Database(metaclass=Singleton):
    def __init__(self, my_id):
        print('Loading database {}. Generated rand of {}'.format(my_id, random.randint(1,101)))

if __name__ == '__main__':
    d1 = Database(1)
    d2 = Database(2)
    print(d1 == d2)

Singleton Testability

import unittest

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 Database(metaclass=Singleton):
    def __init__(self):
        self.population = {}
        f = open('capitals.txt', 'r')
        lines = f.readlines()
        for i in range(0, len(lines), 2):
            self.population[lines[i].strip()] = int(lines[i + 1].strip())
        f.close()

class SingletonRecordFinder:
    def total_population(self, cities):
        result = 0
        for c in cities:
            result += Database().population[c]
        return result

class ConfigurableRecordFinder:
    def __init__(self, db):
        self.db = db
    def total_population(self, cities):
        result = 0
        for c in cities:
            result += self.db.population[c]
        return result

class DummyDatabase:
    population = {
        'alpha': 1,
        'beta': 2,
        'gamma': 3
    }
    def get_population(self, name):
        return self.population[name]

class SingletonTests(unittest.TestCase):
    """ These test on a live database :( """
    """
    # require capitals.txt to work
    def test_is_singleton(self):
        db = Database()
        db2 = Database()
        self.assertEqual(db, db2)
    def test_singleton_total_population(self):
        rf = SingletonRecordFinder()
        names = ['Seoul', 'Mexico City']
        tp = rf.total_population(names)
        self.assertEqual(tp, 17500000 + 17400000)  # what if these change?
    """
    def test_dependent_total_population(self):
        ddb = DummyDatabase()
        crf = ConfigurableRecordFinder(ddb)
        self.assertEqual(crf.total_population(['alpha', 'beta']), 3)

if __name__ == '__main__':
    unittest.main()

Adapter

  • The Adapter pattern tries to adapt the interface that you are given to the interface that you actually need/want.

  • The adapter works as a component that give us the interface we require from the interface we have

  • Adapter is a SW construct that adapts existent interface X to conform to the required interface Y

  • The adapter is an in-between, but separate, component/class to interface two components (classes)

  • To implement an adapter first identify the API/class you have and the API/class/method/function you need, then create a component, the adapter, that has a reference to the API/class you have, the adaptee, it wraps the API/class you have and uses it, finally in your system or application you use the adapter which internally will end up using the adaptee

Adapter No-Caching implementation

class Point:
    def __init__(self, x, y):
        self.y = y
        self.x = x
    def __str__(self):
        return f"(x={self.x},y={self.y})"

def draw_point(p):
    print('*', end='')
""" you are given this API and you cannot change it """

class Line:
    def __init__(self, start, end):
        self.end = end
        self.start = start

class Rectangle(list):
    """ Represented as a list of lines. """
    def __init__(self, x, y, width, height):
        super().__init__()
        self.append(Line(Point(x, y), Point(x + width, y)))
        self.append(Line(Point(x + width, y), Point(x + width, y + height)))
        self.append(Line(Point(x, y), Point(x, y + height)))
        self.append(Line(Point(x, y + height), Point(x + width, y + height)))

class LineToPointAdapter(list):
    count = 0
    def __init__(self, line):
        left = min(line.start.x, line.end.x)
        right = max(line.start.x, line.end.x)
        top = max(line.start.y, line.end.y)
        bottom = min(line.start.y, line.end.y)

        LineToPointAdapter.count += 1
        out = f'{self.count}: Generating points for line '
        out +=  f'[{line.start.x},{line.start.y}]→'
        out +=  f'[{line.end.x},{line.end.y}] '
        out +=  f'left: {left} ; right: {right} '
        out +=  f'top: {top} ; bottom: {bottom} '
        out += '| points for line: '

        if right == left:
            for y in range(bottom, top+1):
                p = Point(left, y)
                out += str(p) + " "
                self.append(p)
        elif top == bottom :
            for x in range(left, right+1):
                p = Point(x, top)
                out += str(p) + " "
                self.append(p)
        print(out)

def draw(rcs):
    print("\n\n--- Drawing some stuff ---\n")
    for rc in rcs:
        for line in rc:
            # No cache = We create repeated objects
            adapter = LineToPointAdapter(line)
            for p in adapter:
                draw_point(p)
            print("")

if __name__ == '__main__':
    rs = [Rectangle(1, 1, 10, 10), Rectangle(3, 3, 6, 6)]
    draw(rs)
    draw(rs)

Adapter Caching Implementation

class Point:
    def __init__(self, x, y):
        self.y = y
        self.x = x
    def __str__(self):
        return f"(x={self.x},y={self.y})"

def draw_point(p):
    print('.', end='')

class Line:
    def __init__(self, start, end):
        self.end = end
        self.start = start

class Rectangle(list):
    """ Represented as a list of lines. """
    def __init__(self, x, y, width, height):
        super().__init__()
        self.append(Line(Point(x, y), Point(x + width, y)))
        self.append(Line(Point(x + width, y), Point(x + width, y + height)))
        self.append(Line(Point(x, y), Point(x, y + height)))
        self.append(Line(Point(x, y + height), Point(x + width, y + height)))

class LineToPointAdapter:
    count = 0
    cache = {}

    def __init__(self, line):
        self.h = hash(line)
        if self.h in LineToPointAdapter.cache:
            return

        super().__init__()
        points = []
        left = min(line.start.x, line.end.x)
        right = max(line.start.x, line.end.x)
        top = max(line.start.y, line.end.y)
        bottom = min(line.start.y, line.end.y)

        LineToPointAdapter.count += 1
        out = f'{LineToPointAdapter.count}: Generating points for line '
        out +=  f'[{line.start.x},{line.start.y}]→'
        out +=  f'[{line.end.x},{line.end.y}] '
        out +=  f'left: {left} ; right: {right} '
        out +=  f'top: {top} ; bottom: {bottom} '
        out += '| points for line: '

        if right == left:
            for y in range(bottom, top+1):
                p = Point(left, y)
                out += str(p) + " "
                points.append(p)
        elif top == bottom :
            for x in range(left, right+1):
                p = Point(x, top)
                out += str(p) + " "
                points.append(p)
        print(out)

        LineToPointAdapter.cache[self.h] = points

    def __iter__(self):
        return iter(LineToPointAdapter.cache[self.h])

def draw(rcs):
    print('Drawing some rectangles...')
    for rc in rcs:
        for line in rc:
            adapter = LineToPointAdapter(line)
            for p in adapter:
                draw_point(p)
            print('')
    print('\n')

if __name__ == '__main__':
    rs = [
        Rectangle(1, 1, 10, 10),
        Rectangle(3, 3, 6, 6)
    ]
    draw(rs)
    draw(rs)

    # can define your own hashes or use the defaults
    #print(hash(Line(Point(1, 1), Point(10, 10))))

Bridge

  • Used to connect components through abstractions

  • This pattern is used to prevent Entity Explosion or Cartesian Product Complexity Explosion which happens when you only rely on inheritance and if you have 2 variables that can be combined you end up with 4 classes e.g. a preemptive and non-preemptive scheduler that can be implemented differently in Linux and Windows so you end up with 4 classes

  • The Bridge patterns used inheritance plus aggregation to decouple the interface from the actual implementation

  • The bridge helps connect to class hierarchies by storing a reference to the base of one of the hierarchies in the other one and use it in the derivative classes, when we create these derivatives we will need to pass an derivative object that will be stored through the reference we mentioned before

  • For scaling we don't need to implement new classes for each combination however we do need to implement a new class and extend the methods/functionality on the hierarchy of classes that is stored as a reference

Bridge Implementation

# Circles and squares
# Each can be rendered in vector or raster form
class Renderer():
    def render_circle(self, radius):
        pass
    def render_square(self, side):
        pass
    # For new class we need to extend this

class VectorRenderer(Renderer):
    def render_circle(self, radius):
        print(f'Drawing a circle of radius {radius}')
    def render_square(self, side):
        print(f'Drawing a square of side {side}')
    # Also extend here

class RasterRenderer(Renderer):
    def render_circle(self, radius):
        print(f'Drawing pixels for circle of radius {radius}')
    def render_square(self, side):
        print(f'Drawing pixels for square of side {side}')
    # And extend here

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

class Square(Shape):
    def __init__(self, renderer, side):
        super().__init__(renderer)
        self.side = side
    def draw(self):
        self.renderer.render_square(self.side)
    def resize(self, factor):
        self.side *= factor

# We will need to create a new class

if __name__ == '__main__':
    raster = RasterRenderer()
    vector = VectorRenderer()
    circle = Circle(vector, 5)
    circle.draw()
    circle.resize(2)

Composite

  • The goal of the composite design patterns is to treat individual component and groups of objects in a uniform way so to provide and identical interface for both aggregates and individual components

  • The composite looks to treat individual components and aggregate objects in the same way, for example if you have a drawing app that allows the user to move a Shape most likely the user will want to move a group of Shape objects so at the implementation level we will want to use the same interface/method/mechanism to do this

  • The composite design pattern is used to treat both single (scalar) and composite objects (groups of objects) in exactly the same way

  • It is a mechanism for treating individual objects and groups of objects in a uniform manner

  • Used for cases where composed and singular objects actually need to provide similar or identical behavior. In cases where you want to be able to treat a group of objects using exactly the same interface as a single object we use the composite pattern

Composite Implementations

Simple Graphical Objects Example

class GraphicObject:
    def __init__(self, color=None):
        self.color = color
        self.children = []
        self._name = 'Group'
    @property
    def name(self):
        return self._name
    def _print(self, items, depth):
        items.append('*' * depth)
        if self.color:
            items.append(self.color)
        items.append(f'{self.name}\n')
        for child in self.children:
            child._print(items, depth + 1)
    def __str__(self):
        items = []
        self._print(items, 0)
        return ''.join(items)

class Circle(GraphicObject):
    @property
    def name(self):
        return 'Circle'

class Square(GraphicObject):
    @property
    def name(self):
        return 'Square'

if __name__ == '__main__':
    drawing = GraphicObject()
    drawing._name = 'My Drawing'
    drawing.children.append(Square('Red'))
    drawing.children.append(Circle('Yellow'))

    group = GraphicObject()  # no name
    group.children.append(Circle('Blue'))
    group.children.append(Square('Blue'))
    drawing.children.append(group)

    print(drawing)

Neurons and Neurons Layers Example

from abc import ABC
from collections.abc import Iterable

class Connectable(Iterable, ABC):
    def connect_to(self, other):
        if self == other:
            return
        for s in self:
            for o in other:
                s.outputs.append(o)
                o.inputs.append(s)

class Neuron(Connectable):
    def __init__(self, name):
        self.name = name
        self.inputs = []
        self.outputs = []
    def __iter__(self):
        yield self # This turns a scalar object into something that's iterable
    def __str__(self):
        return 'name: {}. {} inputs: ({}) ; {} outputs: ({}) '.format(self.name, len(self.inputs), [n.name for n in self.inputs], len(self.outputs), [n.name for n in self.outputs])

class NeuronLayer(list, Connectable):
    def __init__(self, name, count):
        super().__init__()
        self.name = name
        for x in range(0, count):
            self.append(Neuron(f'{name}-{x}'))
    def __str__(self):
        return '{} with {} neurons:\n {}'.format(self.name, len(self), "\n ".join([str(n) for n in self]))

if __name__ == '__main__':
    neuron1 = Neuron('n1')
    neuron2 = Neuron('n2')
    layer1 = NeuronLayer('L1', 3)
    layer2 = NeuronLayer('L2', 4)
    neuron1.connect_to(neuron2)
    neuron1.connect_to(layer1)
    layer1.connect_to(neuron2)
    layer1.connect_to(layer2)

    print(neuron1)
    print(neuron2)
    print(layer1)
    print(layer2)

Decorator

  • The Decorator design pattern exists so that you can add additional behaviors to particular classes or functions without either modifying the classes themselves or indeed inheriting from them.

  • It is used when you want to augment a class with extra features but don't want to rewrite or override existing code (as you would do through inheritance)

  • To interact with existent structures a decorator references the decorated object(s) or function(s), so you have a reference and offer additional operations on top of that

  • A decorators facilitates the addition of behavior to individual objects without inheriting from them

  • A decorator keeps a reference to the decorated object(s), the objects it actually decorates, then it adds a certain utility methods and attributes to increase the objects functionality

Functional decorator

  • The implementation of decorator inside python is what is known as a functional decorator which is very specific implementation of the general purpose decorator

  • The functional decorator are used for performing something around a function. So you take a function or indeed a method and what you do is you use the decorators to actually perform some initialization code, perform some termination code and even store some values if you want.

  • Python provides special syntax to apply a functional decorator using the @ symbol

import time

def time_it(func):
  def wrapper():
    start = time.time()
    result = func()
    end = time.time()
    print(f'{func.__name__} took {int((end-start)*1000)}ms')
  return wrapper

def some_op_without_at():
  print('Starting op')
  time.sleep(1)
  print('We are done')
  return 123

@time_it
def some_op_with_at():
  print('Starting op')
  time.sleep(1)
  print('We are done')
  return 123

if __name__ == '__main__':
    # Without using @
    time_it(some_op_without_at)()
    print("")
    # Using @ special python notation
    some_op_with_at()

Classical Decorator

  • You built a class that augments the functionality of the existing class and keep a reference to the class you are augmenting (NOT inherit from the class you augment)

  • Classical decorators is a class which takes the decorated object as an argument, usually also takes some additional values and it provides the extra functionality

from abc import ABC

class Shape(ABC):
    def __str__(self):
        return ''

class Circle(Shape):
    def __init__(self, radius=0.0):
        self.radius = radius
    def resize(self, factor):
        self.radius *= factor
    def __str__(self):
        return f'A circle of radius {self.radius}'

class Square(Shape):
    def __init__(self, side):
        self.side = side
    def __str__(self):
        return f'A square with side {self.side}'

class ColoredShape(Shape):
    def __init__(self, shape, color):
        if isinstance(shape, ColoredShape):
            raise Exception('Cannot apply ColoredDecorator twice')
        self.shape = shape
        self.color = color
    def __str__(self):
        return f'{self.shape} has the color {self.color}'

class TransparentShape(Shape):
    def __init__(self, shape, transparency):
        self.shape = shape
        self.transparency = transparency
    def __str__(self):
        return f'{self.shape} has {self.transparency * 100.0}% transparency'

if __name__ == '__main__':
    circle = Circle(2)
    print(circle)

    red_circle = ColoredShape(circle, "red")
    print(red_circle)

    # ColoredShape doesn't have resize()
    # red_circle.resize(3) # We cannot do this

    red_half_transparent_square = TransparentShape(red_circle, 0.5)
    print(red_half_transparent_square)

    # prevent double application at ColoredShape constructor
    mixed = ColoredShape(ColoredShape(Circle(3), 'red'), 'blue')
    print(mixed)

Dynamic Decorator

  • The idea of a dynamic decorator is to wrap an object (reference to an object), add new attributes and methods but also forward calls to the underlying object or stored reference to use the attributes and methods of this object when we want to access methods and attributes we don't define in the decorator

  • At the implementation level it means to have a reference to an object and override the methods __iter__, __next__, __getattr__, __setattr__, __delattr__, that way we can use all the methods and attributes from the reference when using the decorator

class FileWithLogging:
  def __init__(self, file):
    self.file = file
  def writelines(self, strings):
    self.file.writelines(strings)
    print(f'wrote {len(strings)} lines')
  def __iter__(self):
    return self.file.__iter__()
  def __next__(self):
    return self.file.__next__()
  def __getattr__(self, item):
    return getattr(self.__dict__['file'], item)
  def __setattr__(self, key, value):
    if key == 'file':
      self.__dict__[key] = value
    else:
      setattr(self.__dict__['file'], key)
  def __delattr__(self, item):
    delattr(self.__dict__['file'], item)

if __name__ == '__main__':
  file = FileWithLogging(open('hello.txt', 'w'))
  file.writelines(['hello', 'world'])
  file.write('testing')
  file.close()

Façade / Facade

  • The main idea is to expose several components through a single interface to balance complexity and presentation/usability

  • The Façade tries to provide a simple and easy to use API over a large and sophisticated body of code that could involve several classes and functions

  • The face tries to expose a single function or object that you call to have a lots of things happen underneath

  • A Facade is build to provide a simplified API over a set of classes/components, optionally expose internal through the facade

Façade / Facade Implementation

class Buffer:
  def __init__(self, width=30, height=20):
    self.width = width
    self.height = height
    self.buffer = [' '] * (width*height)
  def __getitem__(self, item):
    return self.buffer.__getitem__(item)
  def write(self, text):
    self.buffer += text

class Viewport:
  def __init__(self, buffer=Buffer()):
    self.buffer = buffer
    self.offset = 0
  def get_char_at(self, index):
    return self.buffer[self.offset+index]
  def append(self, text):
    self.buffer += text

# this is the facade
class Console:
  def __init__(self):
    b = Buffer()
    self.current_viewport = Viewport(b)
    self.buffers = [b]
    self.viewports = [self.current_viewport]
  # high-level
  def write(self, text):
    self.current_viewport.buffer.write(text)
  # low-level
  def get_char_at(self, index):
    return self.current_viewport.get_char_at(index)

if __name__ == '__main__':
  c = Console()
  c.write('hello')
  ch = c.get_char_at(600)
  print(ch)

Flyweight

  • The Flyweight is a space optimization technique which means it is a pattern that tries to save memory

  • The goal of the Flyweight design patterns is to avoid redundancy when storing data, in other words avoid duplication of data. The patterns instead will look to store only references to a data structure shared among many components instead of replicating and storing the data in different components. e.g. just store database ID where the data is stored

It is a space optimization technique that lets us use less memory by storing externally the data associated with similar components/objects

  • The idea is to store common data externally, then specify an index or reference into the external data store, if needed define a range in case you need to apply to a set of elements

Flyweight Implementation

import random
import string
import sys

class User: # Will duplicate storage of strings
    def __init__(self, name):
        self.name = name

class UserFlyweight: # Won't duplicate storage of strings
    strings = []
    def __init__(self, full_name):
        def get_or_add(s):
            if s in self.strings:
                return self.strings.index(s)
            else:
                self.strings.append(s)
                return len(self.strings)-1
        self.names = [get_or_add(x) for x in full_name.split(' ')]
        print("Created user {} with indexes {} user: {}".format(id(self), ":".join([str(i) for i in self.names]), str(self)))

    def __str__(self):
        return ' '.join([self.strings[x] for x in self.names])

def random_string():
    chars = string.ascii_lowercase
    return ''.join([random.choice(chars) for x in range(8)])


if __name__ == '__main__':
    USERS_NUM = 2
    users = []

    u2 = UserFlyweight('Jim Jones')
    u3 = UserFlyweight('Frank Jones')

    first_names = [random_string() for x in range(USERS_NUM//2)]
    last_names = [random_string() for x in range(USERS_NUM//2)]
    for first in first_names:
        for last in last_names:
            usr = UserFlyweight(f'{first} {last}')
            users.append(usr)

    print("Strings:", UserFlyweight.strings)
class FormattedText:
    def __init__(self, plain_text):
        self.plain_text = plain_text
        self.caps = [False] * len(plain_text)
    def capitalize(self, start, end):
        for i in range(start, end):
            self.caps[i] = True
    def __str__(self):
        result = []
        for i in range(len(self.plain_text)):
            c = self.plain_text[i]
            result.append(c.upper() if self.caps[i] else c)
        return ''.join(result)

class BetterFormattedText:
    class TextRange: # This is the Flyweight
        def __init__(self, start, end, capitalize=False, bold=False, italic=False):
            self.end = end
            self.bold = bold
            self.capitalize = capitalize
            self.italic = italic
            self.start = start
        def covers(self, position):
            return self.start <= position <= self.end

    def __init__(self, plain_text):
        self.plain_text = plain_text
        self.formatting = []

    def get_range(self, start, end):
        range = self.TextRange(start, end)
        self.formatting.append(range)
        return range # return the Flyweight

    def __str__(self):
        result = []
        for i in range(len(self.plain_text)):
            c = self.plain_text[i]
            for r in self.formatting:
                if r.covers(i) and r.capitalize:
                    c = c.upper()
            result.append(c)
        return ''.join(result)


if __name__ == '__main__':
    ft = FormattedText('This is a brave new world')
    ft.capitalize(10, 15)
    print(ft)

    bft = BetterFormattedText('This is a brave new world')
    bft.get_range(16, 19).capitalize = True
    bft.get_range(0, 3).capitalize = True
    print(bft)

Proxy

  • A Proxy is a class that functions as an interface to a particular resource. That resource could be other component that can be remote, expensive to construct, require some other functionality, etc

  • The intention of the proxy is to provide the same interface as the component it wraps but with an entirely different behavior or with added functionality, so to create proxy first replicate the existing interface of an object and then add relevant functionality

Protection Proxy

  • A Protection Proxy is a class that controls access to a particular resource. It adds access control functionality by encapsulating the underlying object as an attribute and then expose the same API as this object or parts of the same API
class Car:
    def __init__(self, driver):
        self.driver = driver
    def drive(self):
        print(f'Car being driven by {self.driver.name}')

class CarProxy:
    def __init__(self, driver):
        self.driver = driver
        self.car = Car(driver)
    def drive(self):
        if self.driver.age >= 18:
            self.car.drive()
        else:
            print('Driver too young')

class Driver:
    def __init__(self, name, age):
        self.name = name
        self.age = age


if __name__ == '__main__':
    car = CarProxy(Driver('John', 12))
    car.drive()

Virtual Proxy

  • It is a proxy that appears to be the underlying object. So it appears to be a fully initialized object but in actual fact it's not, it's just masquerading the underlying functionality

  • By virtual it means that for all intents and purposes it appears like the object it's supposed to represent but behind the scenes it can offer additional functionality and behave differently

class Bitmap:
    def __init__(self, filename):
        self.filename = filename
        print(f'Loading image from {filename}')
    def draw(self):
        print(f'Drawing image {self.filename}')

class LazyBitmap: # This is the Virtual Proxy
    def __init__(self, filename):
        self.filename = filename
        self.bitmap = None
    def draw(self):
        if not self.bitmap:
            self.bitmap = Bitmap(self.filename)
        self.bitmap.draw()

def draw_image(image):
    print('About to draw image')
    image.draw()
    print('Done drawing image')

if __name__ == '__main__':
    bmp = LazyBitmap('facepalm.jpg')  # Bitmap
    draw_image(bmp)

Proxy vs Decorator

  • The proxy provides an identical interface to the object that its proxy things to (the objects is uses) whereas the decorator provides an enhanced interface.

  • A decorator may replicate some of its API s or indeed all of its API but it is designed to add additional operations or functionality

  • A decorator usually has a reference to the object that is decorating, taking that objects as a constructor argument the proxy usually doesn't take this reference as argument but constructs the object if is needed

Chain Of responsibility

  • Chain of Responsibility refers to a chain of components who all get a chance to process a command or query, optionally with as default processing implementation and ability yo terminate the processing chain

  • It is a design pattern used to describe what component should be responsible of certain processing when we have multiple components that have a relation to each other, usually in a hierarchy, for example a button inside a group of buttons inside a window

  • This can be implemented as a linked list of references typically references to methods/functions

  • Command-Query Separation (CQS) refers to the idea that whenever we operate on objects we separate invocations into query and command so we have separate means to send commands and queries

    • command Something you call when asking for an action or change (e.g. please double your attack value) so you usually send what you want to change (ID) and a parameter with the new value or something that signals by how much you should modify something
    • query Something you call when asking for information (e.g. please give me your attack value)
  • By using the CQS and the Chain of responsibility you can have listeners to the commands and queries to do something or even override the behaviors of the command or query

class Creature:
    def __init__(self, name, attack, defense):
        self.defense = defense
        self.attack = attack
        self.name = name
    def __str__(self):
        return f'{self.name} ({self.attack}/{self.defense})'

class CreatureModifier:
    def __init__(self, creature):
        self.creature = creature
        self.next_modifier = None
    def add_modifier(self, modifier):
        if self.next_modifier:
            self.next_modifier.add_modifier(modifier)
        else:
            self.next_modifier = modifier
    def remove_modifier(self, modifier):
        if self.next_modifier and modifier is self.next_modifier:
            self.next_modifier = modifier.next_modifier
            del(modifier)
        elif self.next_modifier:
            self.next_modifier.remove_modifier(modifier)
    def handle(self):
        if self.next_modifier:
            self.next_modifier.handle()

class NoBonusesModifier(CreatureModifier):
    def handle(self):
        print('No bonuses for you!')

class DoubleAttackModifier(CreatureModifier):
    def handle(self):
        print(f"Doubling {self.creature.name}'s attack with modifier {id(self)}")
        self.creature.attack *= 2
        super().handle()

class DoubleDefenseModifier(CreatureModifier):
    def handle(self):
        print(f"Doubling {self.creature.name}'s defense with modifier {id(self)}")
        self.creature.defense *= 2
        super().handle()


if __name__ == '__main__':
    goblin = Creature('Goblin', 1, 1)
    print(goblin)

    root = CreatureModifier(goblin)

    # If you uncomment this will stop the chain of
    # responsability by not calling base class handle()
    #root.add_modifier(NoBonusesModifier(goblin))

    root.add_modifier(DoubleAttackModifier(goblin))
    root.add_modifier(DoubleAttackModifier(goblin))
    to_rem = DoubleAttackModifier(goblin)
    root.add_modifier(to_rem)
    root.remove_modifier(to_rem)
    root.add_modifier(DoubleDefenseModifier(goblin))

    root.handle()  # apply modifiers
    print(goblin)
  • An Event broker is a centralized construct or component that we use to propagate queries
# 1) event broker
# 2) command-query separation (cqs)
# 3) observer
from abc import ABC
from enum import Enum

class Event(list):
    def __call__(self, *args, **kwargs):
        for item in self:
            item(*args, **kwargs)

class WhatToQuery(Enum):
    ATTACK = 1
    DEFENSE = 2

class Query:
    def __init__(self, creature_name, what_to_query, default_value):
        self.value = default_value  # bidirectional
        self.what_to_query = what_to_query
        self.creature_name = creature_name

class Game: # This is the event broker
    def __init__(self):
        self.queries = Event()
    def perform_query(self, sender, query):
        self.queries(sender, query)

class Creature:
    def __init__(self, game, name, attack, defense):
        self.initial_defense = defense
        self.initial_attack = attack
        self.name = name
        self.game = game
    @property
    def attack(self):
        q = Query(self.name, WhatToQuery.ATTACK, self.initial_attack)
        self.game.perform_query(self, q)
        return q.value
    @property
    def defense(self):
        q = Query(self.name, WhatToQuery.DEFENSE, self.initial_attack)
        self.game.perform_query(self, q)
        return q.value
    def __str__(self):
        return f'{self.name} ({self.attack}/{self.defense})'

class CreatureModifier(ABC):
    def __init__(self, game, creature):
        self.creature = creature
        self.game = game
        self.game.queries.append(self.handle)
    def handle(self, sender, query):
        pass
    def __enter__(self): # Executed when you enter a `with` block, what you return here is what is assigned to identifier after `as`
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):  # Executed when you exit  a `with` block
        self.game.queries.remove(self.handle)

class DoubleAttackModifier(CreatureModifier):
    def handle(self, sender, query):
        if (sender.name == self.creature.name and
                query.what_to_query == WhatToQuery.ATTACK):
            query.value *= 2

class IncreaseDefenseModifier(CreatureModifier):
    def handle(self, sender, query):
        if (sender.name == self.creature.name and
                query.what_to_query == WhatToQuery.DEFENSE):
            query.value += 3


if __name__ == '__main__':
    game = Game()
    goblin = Creature(game, 'Dark Goblin', 2, 2)
    print(goblin)
    dam = DoubleAttackModifier(game, goblin)
    idm = IncreaseDefenseModifier(game, goblin)
    print(goblin)

    goblin2 = Creature(game, 'Light Goblin', 2, 2)
    print(goblin2)
    with DoubleAttackModifier(game, goblin2):
        print(goblin2)
        with IncreaseDefenseModifier(game, goblin2):
            print(goblin2)
        print(goblin2)

    print(goblin2)

Command

  • Used to define an object that represents an operation and record this operation actually took place

  • A Command is an object that represents an instruction/operation to perform a particular action. This object usually contains all the information necessary for the action to be taken

  • To implement this you encapsulate all the details of an operation in an object, then you define the instruction for applying the command invoke & undo which will call the methods to perform the actual operation and always use the instruction to perform operations

from abc import ABC
from enum import Enum

class BankAccount:
    OVERDRAFT_LIMIT = -500
    def __init__(self, balance=0):
        self.balance = balance
    def deposit(self, amount):
        self.balance += amount
        print(f'Deposited {amount}, balance = {self.balance}')
    def withdraw(self, amount):
        if self.balance - amount >= BankAccount.OVERDRAFT_LIMIT:
            self.balance -= amount
            print(f'Withdrew {amount}, balance = {self.balance}')
            return True
        return False
    def __str__(self):
        return f'Balance = {self.balance}'

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

class BankAccountCommand(Command):
    def __init__(self, account, action, amount):
        self.amount = amount
        self.action = action
        self.account = account
        self.success = None
    class Action(Enum):
        DEPOSIT = 0
        WITHDRAW = 1
    def invoke(self):
        if self.action == self.Action.DEPOSIT:
            self.account.deposit(self.amount)
            self.success = True
        elif self.action == self.Action.WITHDRAW:
            self.success = self.account.withdraw(self.amount)
    def undo(self):
        if not self.success:
            return
        # strictly speaking this is not correct
        # (you don't undo a deposit by withdrawing)
        # but it works for this demo, so...
        if self.action == self.Action.DEPOSIT:
            self.account.withdraw(self.amount)
        elif self.action == self.Action.WITHDRAW:
            self.account.deposit(self.amount)


if __name__ == '__main__':
    ba = BankAccount()
    cmd = BankAccountCommand(ba, BankAccountCommand.Action.DEPOSIT, 100)
    cmd.invoke()
    print('After $100 deposit:', ba)
    cmd.undo()
    print('$100 deposit undone:', ba)
    illegal_cmd = BankAccountCommand(ba, BankAccountCommand.Action.WITHDRAW, 1000)
    illegal_cmd.invoke()
    print('After impossible withdrawal:', ba)
    illegal_cmd.undo()
    print('After undo:', ba)
  • The Composite Command patterns is used when we want to chain commands in which the next command depends on the previous command, in other words you need the previous command to be succesful to continue with tthe following command
from abc import ABC
from enum import Enum

class BankAccount:
    OVERDRAFT_LIMIT = 0
    def __init__(self, balance=0, name_id=""):
        self.balance = balance
        self.name_id = name_id
    def deposit(self, amount):
        self.balance += amount
        print(f'Deposited {amount} from account with id {self.name_id}, balance = {self.balance}')
    def withdraw(self, amount):
        if self.balance - amount >= BankAccount.OVERDRAFT_LIMIT:
            self.balance -= amount
            print(f'Withdrew {amount} from account with id {self.name_id}, balance = {self.balance}')
            return True
        return False
    def __str__(self):
        return f'Balance = {self.balance}'

class Command(ABC):
    def __init__(self):
        self.success = False
    def invoke(self):
        pass
    def undo(self):
        pass

class BankAccountCommand(Command):
    def __init__(self, account, action, amount):
        super().__init__()
        self.amount = amount
        self.action = action
        self.account = account
    class Action(Enum):
        DEPOSIT = 0
        WITHDRAW = 1
    def invoke(self):
        if self.action == self.Action.DEPOSIT:
            self.account.deposit(self.amount)
            self.success = True
        elif self.action == self.Action.WITHDRAW:
            self.success = self.account.withdraw(self.amount)
    def undo(self):
        if not self.success:
            return
        # strictly speaking this is not correct
        # (you don't undo a deposit by withdrawing)
        # but it works for this demo, so...
        if self.action == self.Action.DEPOSIT:
            self.account.withdraw(self.amount)
        elif self.action == self.Action.WITHDRAW:
            self.account.deposit(self.amount)

# try using this before using MoneyTransferCommand!
class CompositeBankAccountCommand(Command, list):
    def __init__(self, items=[]):
        super().__init__()
        for i in items:
            self.append(i)
    def invoke(self):
        for x in self:
            x.invoke()
    def undo(self):
        for x in reversed(self):
            x.undo()

class MoneyTransferCommand(CompositeBankAccountCommand):
    def __init__(self, from_acct, to_acct, amount):
        super().__init__([
            BankAccountCommand(from_acct,
                               BankAccountCommand.Action.WITHDRAW,
                               amount),
            BankAccountCommand(to_acct,
                               BankAccountCommand.Action.DEPOSIT,
                               amount)])
    def invoke(self):
        ok = True
        for cmd in self:
            if ok:
                cmd.invoke()
                ok = cmd.success
            else:
                cmd.success = False
        self.success = ok
    def undo(self):
        ok = True
        for cmd in reversed(self):
            if ok:
                cmd.undo()
                ok = cmd.success
            else:
                cmd.success = False
        self.success = ok

def test_composite_deposit():
    ba = BankAccount()
    deposit1 = BankAccountCommand(ba, BankAccountCommand.Action.DEPOSIT, 1000)
    deposit2 = BankAccountCommand(ba, BankAccountCommand.Action.DEPOSIT, 1000)
    composite = CompositeBankAccountCommand([deposit1, deposit2])
    composite.invoke()
    print(ba)
    composite.undo()
    print(ba)

def test_transfer_fail():
    ba1 = BankAccount(100)
    ba2 = BankAccount()
    # composite isn't so good because of failure
    amount = 1000  # try 1000: no transactions should happen
    wc = BankAccountCommand(ba1, BankAccountCommand.Action.WITHDRAW, amount)
    dc = BankAccountCommand(ba2, BankAccountCommand.Action.DEPOSIT, amount)
    transfer = CompositeBankAccountCommand([wc, dc])
    transfer.invoke()
    print('ba1:', ba1, 'ba2:', ba2)  # end up in incorrect state
    transfer.undo()
    print('ba1:', ba1, 'ba2:', ba2)

def test_better_tranfer():
    ba1 = BankAccount(100, "BA1")
    ba2 = BankAccount(0, "BA2")
    amount = 10
    transfer = MoneyTransferCommand(ba1, ba2, amount)
    transfer.invoke()
    print('ba1:', ba1, 'ba2:', ba2)
    print(transfer.success)
    transfer.undo()
    print('ba1:', ba1, 'ba2:', ba2)
    print(transfer.success)


if __name__ == '__main__':
    test_better_tranfer()

Interpreter

  • Used to process/interpret textual input (e.g. process HTML, XML or simple math expression 3+4/5). It has the goal of turning strings or plain text into data structures

  • The Interpreter is a component that processes structured text data, it does this in two stages: lexing and parsing

    1. lexing: takes the text and split into separate lexical tokens
    2. parsing: interprets the sequence of tokens and converts to a data structure (objects, lists, dicts, strings, etc.)
from enum import Enum

class Token:
    class Type(Enum):
        INTEGER = 0
        PLUS = 1
        MINUS = 2
        LPAREN = 3
        RPAREN = 4
    def __init__(self, type, text):
        self.type = type
        self.text = text
    def __str__(self):
        return f'`{self.text}`'

# ↓↓↓ lexing ↓↓↓
def lex(input):
    result = []
    i = 0
    while i < len(input):
        if input[i] == '+':
            result.append(Token(Token.Type.PLUS, '+'))
        elif input[i] == '-':
            result.append(Token(Token.Type.MINUS, '-'))
        elif input[i] == '(':
            result.append(Token(Token.Type.LPAREN, '('))
        elif input[i] == ')':
            result.append(Token(Token.Type.RPAREN, ')'))
        else:  # must be a number
            digits = [input[i]]
            for j in range(i + 1, len(input)):
                if input[j].isdigit():
                    digits.append(input[j])
                    i += 1
                else:
                    result.append(Token(Token.Type.INTEGER,
                                        ''.join(digits)))
                    break
        i += 1
    return result

# ↓↓↓ parsing ↓↓↓
class Integer:
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return f"Integer {self.value}"

class BinaryOperation:
    class Type(Enum):
        ADDITION = 0
        SUBTRACTION = 1
    def __init__(self):
        self.type = None
        self.left = None
        self.right = None
    @property
    def value(self):
        left = self.left.value
        right = self.right.value
        print(f"Calling value of Binary Op with ID {id(self)} left: {left} ; right: {right}")
        if self.type == self.Type.ADDITION:
            return left + right
        elif self.type == self.Type.SUBTRACTION:
            return left - right

def parse(tokens):
    result = BinaryOperation()
    have_lhs = False
    i = 0
    while i < len(tokens):
        token = tokens[i]

        if token.type == Token.Type.INTEGER:
            integer = Integer(int(token.text))
            if not have_lhs:
                result.left = integer
                have_lhs = True
            else:
                result.right = integer
        elif token.type == Token.Type.PLUS:
            result.type = BinaryOperation.Type.ADDITION
        elif token.type == Token.Type.MINUS:
            result.type = BinaryOperation.Type.SUBTRACTION
        elif token.type == Token.Type.LPAREN:  # note: no if for RPAREN
            j = i
            while j < len(tokens):
                if tokens[j].type == Token.Type.RPAREN:
                    break
                j += 1
            # preprocess subexpression
            subexpression = tokens[i + 1:j]
            element = parse(subexpression)
            if not have_lhs:
                result.left = element
                have_lhs = True
            else:
                result.right = element
            i = j  # advance
        i += 1
    return result

def my_eval(input):
    tokens = lex(input)
    print(' '.join(map(str, tokens)))

    parsed = parse(tokens)
    print(f'{input} = {parsed.value}')

if __name__ == '__main__':
    my_eval('(13+4)-(12+1)')
    my_eval('1+(3-4)')

    # This won't work
    print("Following evaluation will be wrong")
    my_eval('1+2+(3-4)')

Iterator / Traversal

  • Iteration is a core functionality of various data structures

  • An Iterator is a class that facilitates a mechanism to traverse a data structure, it specified how to traverse an object. This is done through two other mechanism:

    1. Current element Reference: The iterator keeps a reference to the current element
    2. Traverse Operation: The iterator knows how to move from the current to a different element
  • In Python if you want an object to be iterable it needs to have an __iter__ method, which needs to expose and iterator while the iterator need to have a __next__ method which returns each of the iterated elements, every time is called you return the current element until you have nothing to return (reach the end) in that case you raise a stop iteration (raise StopIteration)

    • in some cases the same object can implement __iter__ and __next__
  • A Stateful iterator changes the current element it stores as reference so every time you call it changes this reference. These type of iterators cannot be recursive

class Node:
  def __init__(self, value, left=None, right=None):
    self.right = right
    self.left = left
    self.value = value
    self.parent = None
    if left:
      self.left.parent = self
    if right:
      self.right.parent = self
  def __iter__(self):
    return InOrderIterator(self)

class InOrderIterator:
  def __init__(self, root):
    self.root = self.current = root
    self.yielded_start = False
    while self.current.left:
      self.current = self.current.left
  def __next__(self):
    if not self.yielded_start:
      self.yielded_start = True
      return self.current
    if self.current.right:
      self.current = self.current.right
      while self.current.left:
        self.current = self.current.left
      return self.current
    else:
      p = self.current.parent
      while p and self.current == p.right:
        self.current = p
        p = p.parent
      self.current = p
      if self.current:
        return self.current
      else:
        raise StopIteration

def traverse_in_order(root):
  def traverse(current):
    if current.left:
      for left in traverse(current.left):
        yield left
    yield current
    if current.right:
      for right in traverse(current.right):
        yield right
  for node in traverse(root):
    yield node


if __name__ == '__main__':
  #   1
  #  / \
  # 2   3
  # in-order: 213
  # preorder: 123
  # postorder: 231

  root = Node(1, Node(2), Node(3))
  it = iter(root)
  print([next(it).value for x in range(3)])

  for x in root:
    print(x.value)

  for y in traverse_in_order(root):
    print(y.value)
  • List-Backed properties / Array-Backed properties refers to the idea of storing the values of attributes in a list or collection, then expose an API to access those values, this is to decrease changes in code when you have operations that involve multiple attributes (e.g. getting the max or min values, getting the average, getting the sum of all, etc.) you still need to implement accessors when you add new properties but the methods that do calculations based on all the attributes will adapt because you use a list, a collection or another data structure that can be iterated
class Creature:
  strength_index = 0
  intelligence_index = 1
  agility_index = 2

  def __init__(self, name, strength=10, intelligence=10, agility=10):
    self.stats = [strength, intelligence, agility]
    self.name = name
  @property
  def strength(self):
    return self.stats[Creature.strength_index]
  @strength.setter
  def strength(self, value):
    self.stats[Creature.strength_index] = value
  @property
  def intelligence(self):
    return self.stats[Creature.intelligence_index]
  @intelligence.setter
  def intelligence(self, value):
    self.stats[Creature.intelligence_index] = value
  @property
  def agility(self):
    return self.stats[Creature.agility_index]
  @agility.setter
  def agility(self, value):
    self.stats[Creature.agility_index] = value
  # Operational methods, could also be properties
  def sum_stats(self):
    return sum(self.stats)
  def max_stats(self):
    return max(self.stats)
  def min_stats(self):
    return min(self.stats)
  def average_stats(self):
    return self.sum_stats()/len(self.stats)

  def __str__(self):
    c_str = f'''{self.name} with
      strength: {self.strength}
      intelligence: {self.intelligence}
      agility: {self.agility}
    stats:
      sum: {self.sum_stats()}
      max: {self.max_stats()}
      min: {self.min_stats()}
      avg: {self.average_stats()}
    '''
    return c_str

if __name__ == '__main__':
  c1 = Creature("Ork")
  print(c1)

Mediator

  • A Mediator is a component that facilitates communication between other componentts without them necessarily being aware of each other or having direct access/reference to each other

  • It used to facilitate communication between components, since components are always going in and out of systems the mediator functions as central component used for communication

  • To implement a mediator:

    1. You have a mediator object
    2. Each object in the system that requires it for communication will refer to it (for example by storing it in a property in the constructor)
    3. The mediator engages in bidirectional communication with it's connected components, this means the mediator has functions the components can call (e.g. broadcast, message, fire, etc.) and components have functions the mediator can call (e.g. receive)
class Person:
    def __init__(self, name):
        self.name = name
        self.chat_log = []
        self.room = None
    def receive(self, sender, message):
        s = f'{sender}: {message}'
        print(f'[{self.name}\'s chat session] {s}')
        self.chat_log.append(s)
    def say(self, message):
        self.room.broadcast(source=self.name, message=message)
    def private_message(self, who, message):
        self.room.message(source=self.name, destination=who, message=message)

class ChatRoom: # This is the mediator
    def __init__(self):
        self.people = []
    def broadcast(self, source, message):
        for p in self.people:
            if p.name != source:
                p.receive(sender=source, message=message)
    def join(self, person):
        join_msg = f'{person.name} joins the chat'
        self.broadcast(source='room', message=join_msg)
        person.room = self
        self.people.append(person)
    def leave(self, person):
        if person in self.people:
            leave_msg = f'{person.name} leaves the chat'
            person.room = None
            self.people.remove(person)
            self.broadcast(source='room', message=leave_msg)
    def message(self, source, destination, message):
        for p in self.people:
            if p.name == destination:
                p.receive(sender=source, message=message)

if __name__ == '__main__':
    room = ChatRoom()
    john = Person('John')
    jane = Person('Jane')
    room.join(john)
    john.say('hi room') # This won't show since nobody else is in the room at this point
    room.join(jane)
    john.say('hi room again')
    jane.say('oh, hey john')

    simon = Person('Simon')
    room.join(simon)
    simon.say('hi everyone!')

    jane.private_message('Simon', 'glad you could join us!')
    room.leave(john)
class Event(list):
    def __call__(self, *args, **kwargs):
        for item in self:
            item(*args, **kwargs)

class Game:
    def __init__(self):
        self.events = Event()
    def fire(self, args):
        self.events(args)

class GoalScoredInfo:
    def __init__(self, who_scored, goals_scored):
        self.goals_scored = goals_scored
        self.who_scored = who_scored

class Player:
    def __init__(self, name, game):
        self.name = name
        self.game = game
        self.goals_scored = 0
    def score(self):
        self.goals_scored += 1
        args = GoalScoredInfo(self.name, self.goals_scored)
        self.game.fire(args)

class Coach:
    def __init__(self, game):
        game.events.append(self.celebrate_goal)
    def celebrate_goal(self, args):
        if isinstance(args, GoalScoredInfo):
            if args.goals_scored == 3:
                print(f'Coach says: You are on fire {args.who_scored}!')
            elif args.goals_scored > 1:
                print(f'Coach says: well done, {args.who_scored}!')
            else:
                pass # coach not impressed after more than 3 goals or just 1 goal


if __name__ == '__main__':
    game = Game()
    player = Player('Sam', game)
    coach = Coach(game)
    player.score()  # ignored by coach
    player.score()  # Coach says: well done, Sam!
    player.score()  # Coach says: You are on fire Sam!

Memento

  • The Memento is a token or handle representing the system state at a point in time, it lets us roll back to this state and could expose state information. So it just a class that typically has no methods and just stores data

  • This pattern is used to keep a memento or snapshot of an object's state to return to that state or just preserve that information

  • To implement it the idea is that every time there is a change on the system you return a token of the current state so that you can restore the system back to the state contained in the token

class Memento:
    def __init__(self, balance):
        self.balance = balance

class BankAccount:
    def __init__(self, balance=0):
        self.balance = balance
        self.changes = [Memento(self.balance)]
        self.current = 0
    def deposit(self, amount):
        self.balance += amount
        m = Memento(self.balance)
        self.changes.append(m)
        self.current += 1
        return m
    def restore(self, memento):
        if memento:
            self.balance = memento.balance
            self.changes.append(memento)
            self.current = len(self.changes)-1
    def undo(self):
        if self.current > 0:
            self.current -= 1
            m = self.changes[self.current]
            self.balance = m.balance
            return m
        return None
    def redo(self):
        if self.current + 1 < len(self.changes):
            self.current += 1
            m = self.changes[self.current]
            self.balance = m.balance
            return m
        return None
    def __str__(self):
        return f'Balance = {self.balance}'


if __name__ == '__main__':
    ba = BankAccount(100)
    m50 = ba.deposit(50)
    ba.deposit(25)
    print(ba)
    ba.undo()
    print(f'Undo 1: {ba}')
    ba.undo()
    print(f'Undo 2: {ba}')
    ba.redo()
    print(f'Redo 1: {ba}')
    ba.redo()
    print(f'Redo 2: {ba}')
    ba.restore(m50)
    print(f'Restore: {ba}')

Observer

  • Used when we want to be informed when certain thing happen (e.g. when an object's property changes, when an object does something, external event happen, etc)

  • The Observer design pattern involves an observer (consumer) component which is an object that wishes to be informed about events happening in the system and one or more observable (producer) components which are the entities generating the events

  • The observer listens to events and gets a notification when they occur, these notifications usually include useful data, to potentially do something with them. It can also unsubscribe from events if it is no longer interested

    • The Observable must provide an event to subscribe
    • The Observer defines a callback and register or unregister this callback to the observable
class Event(list):
    def __call__(self, *args, **kwargs):
        for item in self:
            item(*args, **kwargs)

class Person:
    def __init__(self, name, address):
        self.name = name
        self.address = address
        self.falls_ill = Event()
    # Event definition
    def catch_a_cold(self):
        self.falls_ill(self.name, self.address)

# callback
def call_doctor(name, address):
    print(f'A doctor has been called to {address}')

if __name__ == '__main__':
    person = Person('Sherlock', '221B Baker St')
    # Subscribe to Event
    person.falls_ill.append(call_doctor)
    # callback define in lambda
    person.falls_ill.append(lambda name, addr: print(f'{name} is ill'))
    # Event happens
    person.catch_a_cold()
    # And you can remove subscriptions too
    person.falls_ill.remove(call_doctor)
    person.catch_a_cold()
  • A property observer tells you whenever a property has changed but have the disadvantage of not scaling when you have properties that depend on each other
class Event(list):
  def __call__(self, *args, **kwargs):
    for item in self:
      item(*args, **kwargs)

class PropertyObservable:
  def __init__(self):
    # this is the event which you need to subscribe
    self.property_changed = Event()

class Person(PropertyObservable):
  def __init__(self, age=0):
    super().__init__()
    self._age = age
  @property
  def age(self):
    return self._age
  @age.setter
  def age(self, value):
    if self._age == value:
      return
    self._age = value
    self.property_changed('age', value)

class TrafficAuthority:
  def __init__(self, person):
    self.person = person
    # Subscribe here
    person.property_changed.append(self.person_changed)
  # This is the callback
  def person_changed(self, name, value):
    if name == 'age':
      if value < 16:
        print('Sorry, you still cannot drive')
      else:
        print('Okay, you can drive now')
        # unsubscribe here
        self.person.property_changed.remove(self.person_changed)


if __name__ == '__main__':
  p = Person()
  ta = TrafficAuthority(p)
  for age in range(14, 20):
    print(f'Setting age to {age}')
    p.age = age

State

  • The State is a pattern in which the object's behavior is determined by its state. This implies the objects is transitioning from one state to another and something needs to trigger a transition

  • The state design pattern is designed to model cases where the things you can do with a component depend on the state of that component and the changes in state can be explicit, by doing something or implicit or in response to an event

  • A State Machine is a manager of states and transitions

  • In the classical OOP implementation of the state design pattern every state is a class and each class handles transitions from one state to another (it is the one that changes the state) however this classical state implementation is not the recommended implementation

from enum import Enum, auto

TRIGGER_INDEX = 0
NEXT_STATE_INDEX = 1

TRANSITIONS_KEY = "transitions"
FUNC_KEY = "function"

class State(Enum):
    OFF_HOOK = auto()
    CONNECTING = auto()
    CONNECTED = auto()
    ON_HOLD = auto()
    ON_HOOK = auto()

class Trigger(Enum):
    CALL_DIALED = auto()
    HUNG_UP = auto()
    CALL_CONNECTED = auto()
    PLACED_ON_HOLD = auto()
    TAKEN_OFF_HOLD = auto()
    LEFT_MESSAGE = auto()
    TAKE_OFF_HOOK = auto()

def simulate_state_function(*args):
    print("State action with arguments: {}".format(args))

if __name__ == '__main__':
    rules = {
        State.OFF_HOOK: {
            "transitions": [(Trigger.CALL_DIALED, State.CONNECTING)],
            "function": simulate_state_function
        },
        State.CONNECTING: {
            "transitions": [(Trigger.HUNG_UP, State.ON_HOOK), (Trigger.CALL_CONNECTED, State.CONNECTED)],
            "function": simulate_state_function
        },
        State.CONNECTED: {
            "transitions": [(Trigger.LEFT_MESSAGE, State.ON_HOOK), (Trigger.HUNG_UP, State.ON_HOOK), (Trigger.PLACED_ON_HOLD, State.ON_HOLD)],
            "function": simulate_state_function
        },
        State.ON_HOLD: {
            "transitions": [(Trigger.TAKEN_OFF_HOLD, State.CONNECTED), (Trigger.HUNG_UP, State.ON_HOOK)],
            "function": simulate_state_function
        },
        State.ON_HOOK: {
            "transitions": [(Trigger.TAKE_OFF_HOOK, State.OFF_HOOK)],
            "function": simulate_state_function
        }
    }

    state = State.OFF_HOOK
    exit_state = State.ON_HOOK # Switch to None to make it an Infinite State Machine

    while True:
        print(f'The phone is currently {state}')
        rules[state][FUNC_KEY](state)

        if state == exit_state:
            break

        for i in range(len(rules[state][TRANSITIONS_KEY])):
            t = rules[state][TRANSITIONS_KEY][i][TRIGGER_INDEX]
            print(f'{i}: {t}')

        idx = int(input('Select a trigger:'))
        s = rules[state][TRANSITIONS_KEY][idx][NEXT_STATE_INDEX]
        state = s

    print('We are done using the phone.')
from enum import Enum, auto

class State(Enum):
    LOCKED = auto()
    FAILED = auto()
    UNLOCKED = auto()

if __name__ == '__main__':
    code = '1234'
    state = State.LOCKED
    entry = ''
    while True:
        if state == State.LOCKED:
            entry += input("Give me a digit of combination: ")
            if entry == code:
                state = State.UNLOCKED
            if not code.startswith(entry):
                # the code is wrong
                state = State.FAILED
        elif state == State.FAILED:
            #print('\nFAILED')
            entry = ''
            state = State.LOCKED
        elif state == State.UNLOCKED:
            print('\nUNLOCKED')
            break

Strategy

  • The Strategy design pattern refers to decomposing a system or algorithm into higher level, which refers to a high level or general description of a process that can be reused and lower level parts, which refers to actual implementation details and specific details of the process

  • The strategy design pattern comes from the idea that system and algorithms can be decomposed into common parts and specific and enables the exact behavior of a system to be selected at run-time

  • The idea of the strategy design pattern is that at run-time certain implementation details, that will be feed into a component, are specified and then a high level components consume this and use the low level strategy to actually do something

  • The strategy is usually a separate chunk of code or component that you can subsequently use inside another object that consumes it to perform an specific operation, the strategy must have a defined interface so that we can switch it

  • To implement the strategy pattern we rely on composition. First you need to define a high-level or general algorithm, then you define the interface you expect each strategy to follow, these are the methods that will be used in the high-level/general algorithm, finally provide a way for the general algorithm to use the strategies usually through composition, holding a reference(s) to one or more strategies

  • Yo have an object that takes a strategy object as parameter and uses that strategy methods

from abc import ABC
from enum import Enum, auto

class OutputFormat(Enum):
    MARKDOWN = auto()
    HTML = auto()

class ListStrategy(ABC):
    def start(self, buffer): pass
    def end(self, buffer): pass
    def add_list_item(self, buffer, item): pass

class MarkdownListStrategy(ListStrategy):
    def add_list_item(self, buffer, item):
        buffer.append(f' * {item}\n')

class HtmlListStrategy(ListStrategy):
    def start(self, buffer):
        buffer.append('
    \n') def end(self, buffer): buffer.append('
\n') def add_list_item(self, buffer, item): buffer.append(f'
  • {item}
  • \n') class TextProcessor: def __init__(self, list_strategy=HtmlListStrategy()): self.buffer = [] self.list_strategy = list_strategy def append_list(self, items): self.list_strategy.start(self.buffer) for item in items: self.list_strategy.add_list_item( self.buffer, item ) self.list_strategy.end(self.buffer) def set_output_format(self, format): if format == OutputFormat.MARKDOWN: self.list_strategy = MarkdownListStrategy() elif format == OutputFormat.HTML: self.list_strategy = HtmlListStrategy() def clear(self): self.buffer.clear() def __str__(self): return ''.join(self.buffer) if __name__ == '__main__': items = ['foo', 'bar', 'baz'] tp = TextProcessor() tp.set_output_format(OutputFormat.MARKDOWN) tp.append_list(items) print(tp) tp.set_output_format(OutputFormat.HTML) tp.clear() tp.append_list(items) print(tp)

    Template Method

    • The Template Method design pattern comes from the same idea that algorithms can be decomposed into common parts and specific parts and implements this idea using inheritance

    • The Template Method allows us to define the skeleton of the algorithm and the concrete implementations details are defined in subclasses

    • To implement the template method pattern you define the overall algorithm in the base class and use 'abstract members' (methods and properties) which are also defined in the base class, then inheritors override these members providing actual implementations finally the template method gets invoked from the base class and all the overridden members are used to perform the specific operations needed

    • You have a template method which defines a skeleton algorithm but has a number of blanks which will be defined in the derivatives, you fill the blanks there then call the template method through the derived reference getting all the benefits of the skeleton algorithm and the overridden blanks

    from abc import ABC
    
    class Game(ABC):
        def __init__(self, number_of_players):
            self.number_of_players = number_of_players
            self.current_player = 0
        def run(self): # This is the template method
            self.start()
            while not self.have_winner:
                self.take_turn()
            print(f'Player {self.winning_player} wins!')
    
        def start(self): pass
        @property
        def have_winner(self): pass
        def take_turn(self): pass
        @property
        def winning_player(self): pass
    
    
    class Chess(Game):
        def __init__(self):
            super().__init__(2)
            self.max_turns = 10
            self.turn = 1
        def start(self):
            print(f'Starting a game of chess with {self.number_of_players} players.')
        @property
        def have_winner(self):
            return self.turn == self.max_turns
        def take_turn(self):
            print(f'Turn {self.turn} taken by player {self.current_player}')
            self.turn += 1
            self.current_player = 1 - self.current_player
        @property
        def winning_player(self):
            return self.current_player
    
    
    if __name__ == '__main__':
        chess = Chess()
        chess.run() # Call the template method form the derive
    

    Visitor

    • The Visitor is a component that knows how to travers a data structure that can be composed of different types, these types can or cannot be related to one another, they could also be in an inheritance tree or some types could contain other types

    • The visitor is a component that allows you to go over a complicated data structure and do appropriate operation on every single type of node it encounters on the data structure

    • Used when you need to define a new operation on a entire class hierarchy and don't want to keep modifying every class in the hierarchy, this means avoid explicit type checks

    • Intrusive Visitor: Modify the actual types that compose a complicated data structure

    class DoubleExpression:
        def __init__(self, value):
            self.value = value
        def print(self, buffer):
            buffer.append(str(self.value))
        def eval(self): return self.value
    
    class AdditionExpression:
        def __init__(self, left, right):
            self.right = right
            self.left = left
        def print(self, buffer):
            buffer.append('(')
            self.left.print(buffer)
            buffer.append('+')
            self.right.print(buffer)
            buffer.append(')')
        def eval(self):
            return self.left.eval() + self.right.eval()
    
    if __name__ == '__main__':
        # represents 1+(2+3)
        e = AdditionExpression(
            DoubleExpression(1),
            AdditionExpression(
                DoubleExpression(2),
                DoubleExpression(3)
            )
        )
        buffer = []
        e.print(buffer)
        print(''.join(buffer), '=', e.eval())
    
        # breaks OCP: requires we modify the entire hierarchy
        # what is more likely: new type or new operation?
    
    • Checking Type/Reflective Visitor: You have a separate object that takes other objects, checks for the type and performs the right operations depending on this

    The Checking of the type is called a reflection operation in some programming languages

    from abc import ABC
    
    class Expression(ABC):
        def print(self, buffer): ExpressionPrinter.print(self, buffer)
    
    class DoubleExpression(Expression):
        def __init__(self, value):
            self.value = value
    
    class AdditionExpression(Expression):
        def __init__(self, left, right):
            self.right = right
            self.left = left
    
    class ExpressionPrinter:
        @staticmethod
        def print(e, buffer):
            """ Will fail silently on a missing case. """
            if isinstance(e, DoubleExpression):
                buffer.append(str(e.value))
            elif isinstance(e, AdditionExpression):
                buffer.append('(')
                ExpressionPrinter.print(e.left, buffer)
                buffer.append('+')
                ExpressionPrinter.print(e.right, buffer)
                buffer.append(')')
        # Equivalent to defined at the ABC level
        # Expression.print = lambda self, b: ExpressionPrinter.print(self, b)
    
    # still breaks OCP because new types require M×N modifications
    if __name__ == '__main__':
        # represents 1+(2+3)
        e = AdditionExpression(
            DoubleExpression(1),
            AdditionExpression(
                DoubleExpression(2),
                DoubleExpression(3)
            )
        )
        buffer = []
        # ExpressionPrinter.print(e, buffer)
        # IDE might complain here
        e.print(buffer)
        print(''.join(buffer))
    
    • Classical Visitor: uses double-dispatch (if needed visit-accept-visit) where you define a visit method for each of the types that will be visited, then you call visit() and the entire structure gets traversed
      • To implement this in python we can use a decorator that allows us to have several methods with identical names but with different type arguments so when you call one of them you are calling on the correct variation
    # taken from https://tavianator.com/the-visitor-pattern-in-python/
    def _qualname(obj):
        """Get the fully-qualified name of an object (including module)."""
        return obj.__module__ + '.' + obj.__qualname__
    
    def _declaring_class(obj):
        """Get the name of the class that declared an object."""
        name = _qualname(obj)
        return name[:name.rfind('.')]
    
    # Stores the actual visitor methods
    _methods = {}
    
    # Delegating visitor implementation
    def _visitor_impl(self, arg):
        """Actual visitor method implementation."""
        method = _methods[(_qualname(type(self)), type(arg))]
        return method(self, arg)
    
    # The actual @visitor decorator
    def visitor(arg_type):
        """Decorator that creates a visitor method."""
        def decorator(fn):
            declaring_class = _declaring_class(fn)
            _methods[(declaring_class, arg_type)] = fn
            # Replace all decorated methods with _visitor_impl
            return _visitor_impl
        return decorator
    # ↑↑↑ LIBRARY CODE ↑↑↑
    
    class DoubleExpression:
        def __init__(self, value):
            self.value = value
    
    class AdditionExpression:
        def __init__(self, left, right):
            self.left = left
            self.right = right
    
    class ExpressionPrinter:
        def __init__(self):
            self.buffer = []
        @visitor(DoubleExpression)
        def visit(self, de):
            self.buffer.append(str(de.value))
        @visitor(AdditionExpression)
        def visit(self, ae):
            self.buffer.append('(')
            self.visit(ae.left)
            self.buffer.append('+')
            self.visit(ae.right)
            self.buffer.append(')')
        def __str__(self):
            return ''.join(self.buffer)
    
    class ExpressionEvaluator:
      def __init__(self):
        self.value = None
      @visitor(DoubleExpression)
      def visit(self, de):
        self.value = de.value
      @visitor(AdditionExpression)
      def visit(self, ae):
        # ae.left.accept(self)
        self.visit(ae.left)
        temp = self.value # need to cache because ExpressionEvaluator is stateful
        # ae.right.accept(self)
        self.visit(ae.right)
        self.value += temp
    
    
    if __name__ == '__main__':
        # represents 1+(2+3)
        e = AdditionExpression(
            DoubleExpression(1),
            AdditionExpression(
                DoubleExpression(2),
                DoubleExpression(3)
            )
        )
        printer = ExpressionPrinter()
        printer.visit(e)
        evaluator = ExpressionEvaluator()
        evaluator.visit(e)
        print(f'{printer} = {evaluator.value}')
    

    References

    Want to show support?

    If you find the information in this page useful and want to show your support, you can make a donation

    Use PayPal

    This will help me create more stuff and fix the existent content...