Notes from watching ‘Design Patterns in Python 3’ from Pluralsight. Goes through a few Structural Patterns.
Adapter Pattern
- Converts an interface of a class into another that the client expects. We end up programing against a single API.
from abc import ABC, abstractproperty
class ICustomer(ABC):
@abstractproperty
def name(self) -> str:
pass
@abstractproperty
def address(self) -> str:
pass
class Customer(ICustomer):
@property
def name(self) -> str:
...
@property
def address(self) -> str:
...
@dataclass
class Vendor:
name: str
number: str
street: str
Object Adapters — Composition
class AbstractCustomerAdapter(ABC, ICustomer):
def __init__(self, adaptee):
self._adaptee = adaptee
@property
def adaptee(self):
return self.adaptee
class VendorAdapter(AbstractCustomerAdapter):
@property
def name(self) -> str:
return self.adaptee.name
@property
def address(self) -> str:
return '{} {}'.format(
self.adaptee.number,
self.adaptee.street
)
items: List[ICustomer] = [
Customers(name='...', address='...'),
VendorAdapter(
Vendor(name='...', number='...', street='...')
),
]
- favor composition over inheritance
- adapter delegates to
adaptee
Bridge
Composite
- Trees and hierarchies — a mini structure resembles the whole structure. Compose objects into tree structures.
- Nodes — Leaf, Composite
from dataclasses import dataclass
from collections import Iterable
@dataclass
class Person:
name: str
class Family(Iterable):
_members = []
def __init__(self, members):
self._members = members
def __iter__(self):
return iter(self._members)
family = Family([
Person(...),
Person(...),
...
])
singles = Family([
Person(...),
...
])
from abc import ABC, abstractmethod
class AbstractComposite(ABC):
@abstractmethod
def get_oldest(self):
pass
## leafs
@dataclass
class Person(AbstractComposite):
name: str
birthdate: date
@property
def get_oldest(self):
return self
class NullPerson(AbstractComposite):
name: str = None
birthdate: date = date.max
@property
def get_oldest(self):
return self
## tree / composite structure
class Tree(Iterable, AbstractComposite):
_members = []
def __init__(self, members):
self._members = members
def __iter__(self):
return iter(self._members)
@property
def get_oldest(self):
## depth first ...
def f(t1, t2):
t1_, t2_ = t1.get_oldest(), t2.get_oldest()
return t1_ if t1_.birthdate < t2_.birthdate else t2_
return reduce(f, self, NullPerson())
## main
Tree([
Tree([
Person(...),
]),
Person(...),
])
Decorator
- adds abilities to an object dynamically at run time. Ends up with a flexible alternative to subclasses.
from abc import ABC, abstractproperty
class AbstractCar(ABC):
@abstractproperty
def description(self):
pass
@abstractproperty
def cost(self):
pass
class EconomyCar(AbstractCar):
@property
def description(self):
return '...'
@property
def cost(self):
return 1000
class AbstractDecorator(AbstractCar):
def __init__(self, car):
self._car = car
@property
def car(self):
return self._car
class V6(AbstractDecorator):
@property
def description(self):
return self._car.description + ', V6'
@property
def cost(self):
return self._car.cost + 800.00
## main
car = EconomyCar()
car = V6(car)
car = Leather(car)
...
- More flexible, keeps things simple. Downside is that lots of little classes exist (leverage Creational Patterns — factory, builder?).
- Use when you want to add new functionality to existing objects (wrap).
Façade
- Putting a nice face on an interface. Useful when combining multiple APIs or reducing complexity with a simpler/unified interface. Create a Higher-level interface.
from abc import ABC, abstractmethod
class AbstractGetEmployeeFacade(ABC):
@abstractmethod
def get_employees(self):
pass
class GetEmployeeFacade(AbstractGetEmployeeFacade):
def get_employee(self):
connection = pyodbc.connect(...)
...
ActiveGetEmployeeFacades = {
'pyodbc': GetEmployeeFacade,
...
}
class GetEmployeeFacadeFactory:
@staticmethod
def create(name):
facade = ActiveGetEmployeeFacades[name]
if name in ActiveGetEmployeeFacades
else NullGetEmployeeFacade
return facade()
api = GetEmployeeFacadeFactory.create('pydoc')
api.get_employee()
- shields clients from subsystem (multiple) details — simple API
- reduces the number of objects we need to handle
Flyweight
lets you fit more objects into memory by sharing common parts of state between multiple objects instead of keeping all of the data in each object.
- state data is centralized which reduces object instances.
Unshared State Example
from dataclasses import dataclass
@dataclass
class Event:
x: int ## x coord
y: int ## y coord
t: float
e: float
def velocity(self):
return ...
## main
events = []
for _ in range(100000):
events.append(
Event(...)
)
Shared State
from abc import ABC
import numpy as np
class AbstractFlyweight(ABC):
@abstractmethod
def get_velocity(self):
pass
class SharedEvents(AbstractFlyweight):
def __init__(self, xaxis, yaxis):
self._events = np.zeros((xaxis, yaxis, 2), np.double)
def set_event(self, x, y, e, t):
self._events[x, y] = (t, e)
def get_event(self, x, y):
## could return an Event type ...
return self._events[x, y]
def get_velocity(self, x, y):
return ...
class FlyweightFactory:
@staticmethod
def create(xaxis, yaxis):
return SharedEvents(x, y)
## main
events = FlyweightFactory.create(10, 10)
for _ in range(100000):
events.set_event(...)
- useful when similar objects put pressure on the CPU and memory of the system.
- combine with factory pattern to supply reusable (repeated) objects
Proxy
- A proxy acts on an object (called subject). Keeps a reference and exposes methods through an interface. In this way, the proxy controls access to the object (subject).
from abc import ABC, abstractmethod
class AbstractEmployeees(ABC):
@abstractmethod
def get_employees_info(self, empids):
pass
EMPLOYEES = {
'<id>': Employee(...),
}
class Employees(AbstractEmployeees):
def get_employees_info(self, empids):
return (
EMPLOYEES[id]
for id in empids
if id in EMPLOYEES
)
class Proxy(AbstractEmployeees):
def __init__(self, employees, reqid):
...
def get_employees_info(self, empids):
acc = AccessControls.get_access_control()
can_see = reqid in acc and acc[reqid].can_see_pid()
for e in self._employees.get_employees_info(empids):
if e.id == reqid or can_see:
## using prototype pattern
yield e.clone()
else:
clone = e.clone()
clone.<personal infor> = '****' ## redact
yield clone
## factory method, client doesn't know about the Proxy
def get_all_employees(reqid) -> AbstractEmployeees:
return Proxy(Employees(), reqid)
## main
for employee in get_all_employees(...):
...
- introduces indirection but allows us to hide details from client
@functools.lru_cache
— virtual proxy implementation- use when you need to add controls to an object