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:
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.
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.
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.
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.
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}')