Skip to content

Principles of Design Patterns

Single Responsibility Principle

This principle states that every class should have one responsibility and therefore one reason to change. We want to avoid 'God' classes which perform all the functionality within a single class. This simplifies modification of code because if you do need to make a change, you only need to change it in the one place that has that responsibility.

Open Closed Principle

This principle follows the rule 'open for extension, closed for modification' after you have created a class or function. Take the below example:

python
class Product:
    def __init__(self, name, color, size):
        self.name = name
        self.color = color
        self.size = size

class ProductFilter:
    def filter_by_color(self, products, color):
        for p in products:
            if p.color == color: yield p

With this design, if you want to create new filters based on product details, you will need to modify your ProductFilter class and this could get out of hand with too many filters. Instead, you would want to design your system so that you can extend classes to add functionality.

python
class Specification:
    def is_satisfied(self, item):
        pass

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

# ... Other specifications

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

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
        ))

Liskov Substitution Principle

This principle states that all implementations using a base class should work correctly for all derived classes. In the below example, the Square class breaks the use_it function because the behavior of Square is different than that off the Rectangle.

python
class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height

    @property
    def width(self): return self._width
    @width.setter
    def width(self, value): self._width = value
    @property
    def height(self): return self._height
    @height.setter
    def height(self, value): self._height = value

    @property
    def area(self): return self._height * self._width

class Square(Rectangle):
    def __init__(self, size):
        Rectangle.__init__(self, size, size)

    @Rectangle.width.setter
    def width(self, value): self._width = self._height = value
    @Rectangle.height.setter
    def height(self, value): self._height = self.width = value

def use_it(rc):
    w = rc.width
    rc.height = 10
    expected = int(w*10)
    print(f'Expected {expected}, got {rc.area}')

use_it(Rectangle(2,3))
use_it(Square(2))

Interface Segregation Principle

This principle states that an interface should not have too many methods and should be broken into multiple interfaces with the minimal amount of functionality. In the below example, subclasses of Machine may not implement all of those functions, like an OldFashionedPrinter can't scan and fax.

python
class Machine:
    def print(self, document):
        raise NotImplementedError
    def fax(self, document):
        raise NotImplementedError
    def scan(self, document):
        raise NotImplementedError

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

class FaxMachine:
    def fax(self, document):
        pass

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

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

Dependency Inversion Principle

This principle states that high level classes should not depend on low level implementations, but instead on interfaces/abstractions. This allows you to swap interfaces for different implementations. In the below example, the Research class depends on Relationships always storing its data in a list. Instead, it should interact with an interface to handle fetching the items it needs.

python
class Relationship(Enum):
    PARENT=0
    CHILD=1
    SIBLING=2

class Person:
    def __init__(self, name):
        self.name = name

# BAD
class Relationships:
    def __init__(self):
        self.relations = []
    def add_parent_and_child(self, parent, child):
        self.relations.append((parent, Relationship.PARENT, child))
        self.relations.append((child, Relationship.CHILD, parent))

class Research:
    def __init__(self, relationships):
        relations = relationships.relations
        for r in relations:
            if r[0].name == 'John' and r[1] == Relationship.PARENT:
                print(f'John has a child called {r[2].name}')

# GOOD
class RelationshipBrowser:
    def find_all_children_of(self, name): pass

class Relationships(RelationshipBrowser):
    # ... same as before but also....
    def find_all_children_of(self, name):
        for r in self.relations:
            if r[0].name == name and r[1] == Relationship.PARENT:
                yield r[2]

class Research:
    def __init__(self, browser):
        for p in browser.find_all_children_of('John'):
            print(f'John has a child called {p}')