Pluralsight Notes — Structural Design Patterns

dp
4 min readJul 8, 2023

--

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
Photo by Alvaro Pinot on Unsplash

--

--