Python: Design Patterns¶
Link: Python: Design Patterns LinkedIn Learning
Understanding Design Patterns¶
Three types of design pattern
- Creational -
polymorphism
- different object from same class - Structural -
inheritance
- relationship between components - Behavioural - Method
Signatures
- objects interacting with each other
Interfaces
help in all these design patterns.
Functional requirement - what job is done by sw, Non-functional - how better sq does the job.
Understanding Pattern Context¶
To use design patterns effectively you need to know the context in which the design patterns work best.
Lets start by learning how to describe the design pattern context. The pattern context consists of the following.
- Participants - the classes involved
- Quality attributes - non functional requirements such as usability, modifiability, reliability, performance and so on. Quality attributes have an impact on the entire software, and architectural solutions address them.
- Forces - various factors or trade-offs you consider when trying to adopt a particular design pattern. Forces are defined by quality attributes.
- Consequences - result of above when using a design pattern
Creational Patterns¶
Factory¶
Factory is an method creating objects
Good when not sure of type of objects required.
Implementation
- a method, returns object based on param.
- in this method, a dict has objects as value and its name as key
- based on param, object is picked from dict value by matching key to param.
class Dog():
def __init__(self, name) -> None:
self._name = name
def speak(self) -> str:
return "Woof!"
class Cat():
def __init__(self, name) -> None:
self._name = name
def speak(self) -> str:
return "Meow!"
# factory method
def get_pet(pet) -> object:
pets = {
"dog": Dog("Moti"),
"cat": Cat("Billu")
}
return pets[pet]
d = get_pet("dog")
print(d.speak())
c = get_pet("cat")
print(c.speak())
Woof! Meow!
Here, you have flexibility to add new class when required, and user is unaffected by this change.
Abstract Factory¶
- Abstract means existance as idea, not physical or concrete.
- It is like layering on top of your classes, so if you have class dog and cat, you can create pet factory class, this can be abstract ?
- real class
Dog
, on top you have classDogFactory
abstract, on top you havePetStore
class
class Dog():
def __init__(self) -> None:
self._name = "Dog"
def speak(self):
return "Whof!"
# factory
class DogFactory():
def get_pet(self):
return Dog()
def get_food(self):
return "Dog Roti"
# abstract factory house
class PetStore():
# abstract factory
def __init__(self, pet_factory=None):
self._pet_factory = pet_factory
def show_pet(self):
# utility to display details
# concrete
pet = self._pet_factory.get_pet()
pet_food = self._pet_factory.get_food()
print(f"Our pet is {pet._name}")
print(f"Our pet says '{pet.speak()}'")
print(f"Its food is '{pet_food}'")
# concrete factory
dog = DogFactory()
# housing abstract
shop = PetStore(dog)
# invoke utility method
shop.show_pet()
Our pet is Dog Our pet says 'Whof!' Its food is 'Dog Roti'
Here, concrete is dog object and dogfood object. class PetStore
is abstract as it is nothing without a concrete object dog
passed to it.
[?] Overcomplicated...!!
Singelton¶
Create only one object from a class. There can be multiple instance, but all share the same attributes. So you implement global variable in OOP style. Useful when you have to cache some data that can can be shared by different things.
"""Singleton implementation"""
class Borg:
_shared_data = {} # attribute dictionary
def __init__(self):
# Make an attribute dictionary
self.__dict__ = self._shared_data
# singleton class, inherits Borg
class Singleton(Borg):
def __init__(self, **kwargs):
Borg.__init__(self)
self._shared_data.update(kwargs)
def __str__(self):
return str(self._shared_data)
x = Singleton(name = "John")
print(x)
y = Singleton(age = 24)
print(y)
{'name': 'John'} {'name': 'John', 'age': 24}
Here, update()
is dict method to update dict with args. Borg, build dict, Singleton uses it and exposes as shared to all Singleton object created. y
automatically has 'name' added, because the object is shared resource.
Builder¶
Helps decomplex when lot of constructors are used. Reduce complexity by divide and conque. It divides roles as:
- Director
- Abstract Builder - interfaces
- Concrete Builder - implements the interfaces
- Product - object being built
# Implement Builder
class Director():
def __init__(self, builder):
self._builder = builder
def construct_car(self):
self._builder.create_new_car()
self._builder.add_model()
self._builder.add_tires()
self._builder.add_engine()
def get_car(self):
return self._builder.car
# Abstract builder
class Builder():
def __init__(self):
self.car = None
def create_new_car(self):
self.car = Car()
# concrete builder
class MustangBuilder(Builder):
def add_model(self):
self.car.model = "Mustang"
def add_tires(self):
self.car.tires = "Radials"
def add_engine(self):
self.car.engine = "Muscle Turbo"
# product
class Car():
def __init__(self):
self.model = None
self.tires = None
self.engine = None
def __str__(self):
return f"{self.model} | {self.tires} | {self.engine}"
# concrete builder
builder = MustangBuilder()
director = Director(builder)
director.construct_car()
car = director.get_car()
print(car)
Mustang | Radials | Muscle Turbo
This makes the process that require same steps more efficient and managable. Now if you have to add new model, it can be done easily.
Prototype¶
Useful when you have to clone things, which makes carbon copy without repeating things.
import copy
class Prototype:
def __init__(self):
self._objects = {}
def register_object(self, name, obj):
"""Register an object"""
self._objects[name] = obj
def unregister_object(self, name):
"""Unregister an object"""
del self._objects[name]
def clone(self, name, **attr):
"""Clone a registered object and update its attributes"""
obj = copy.deepcopy(self._objects.get(name))
obj.__dict__.update(attr)
return obj
class Car:
def __init__(self):
self.name = "Skylark"
self.color = "Red"
self.options = "Ex"
def __str__(self):
return '{} | {} | {}'.format(self.name, self.color, self.options)
c = Car()
prototype = Prototype()
prototype.register_object('skylark',c)
c1 = prototype.clone('skylark')
print(c1)
Skylark | Red | Ex
# Decorator Pattern
from functools import wraps
def make_blink(function):
"""Defines the decorator"""
#This makes the decorator transparent in terms of its name and docstring
@wraps(function)
#Define the inner function
def decorator():
#Grab the return value of the function being decorated
ret = function()
#Add new functionality to the function being decorated
return "<blink>" + ret + "</blink>"
return decorator
#Apply the decorator here!
@make_blink
def hello_world():
"""Original function! """
return "Hello, World!"
#Check the result of decorating
print(hello_world())
#Check if the function name is still the same name of the function being decorated
print(hello_world.__name__)
#Check if the docstring is still the same as that of the function being decorated
print(hello_world.__doc__)
<blink>Hello, World!</blink> hello_world Original function!
Here, we have added brink around string without modifying the original function.
Porxy¶
When there is resource intensive object, we can delay its creation or limit its creation and instead let proxy be used instead. Adapter and Decorator are related to the proxy design pattern.
# Proxy Implementation
import time
class Producer:
"""Define the 'resource-intensive' object to instantiate!"""
def produce(self):
print("Producer is working hard!")
def meet(self):
print("Producer has time to meet you now!")
class Proxy:
""""Define the 'relatively less resource-intensive' proxy to instantiate as a middleman"""
def __init__(self):
self.occupied = 'No'
self.producer = None
def produce(self):
"""Check if Producer is available"""
print("Artist checking if Producer is available ...")
if self.occupied == 'No':
#If the producer is available, create a producer object!
self.producer = Producer()
time.sleep(2)
#Make the prodcuer meet the guest!
self.producer.meet()
else:
#Otherwise, don't instantiate a producer
time.sleep(2)
print("Producer is busy!")
#Instantiate a Proxy
p = Proxy()
#Make the proxy: Artist produce until Producer is available
p.produce()
#Change the state to 'occupied'
p.occupied = 'Yes'
#Make the Producer produce
p.produce()
Artist checking if Producer is available ... Producer has time to meet you now! Artist checking if Producer is available ... Producer is busy!
Adapter Pattern¶
- Lets interface used with different name, makes it simple for client to use different named function on server.
- Bridges and decorators are relared to this
class Korean:
"""Korean speaker"""
def __init__(self):
self.name = "Korean"
def speak_korean(self):
return "An-neyong?"
class British:
"""English speaker"""
def __init__(self):
self.name = "British"
#Note the different method name here!
def speak_english(self):
return "Hello!"
class Adapter:
"""This changes the generic method name to individualized method names"""
def __init__(self, object, **adapted_method):
"""Change the name of the method"""
self._object = object
#Add a new dictionary item that establishes the mapping between the generic method name: speak() and the concrete method
#For example, speak() will be translated to speak_korean() if the mapping says so
self.__dict__.update(adapted_method)
def __getattr__(self, attr):
"""Simply return the rest of attributes!"""
return getattr(self._object, attr)
#List to store speaker objects
objects = []
#Create a Korean object
korean = Korean()
#Create a British object
british = British()
#Append the objects to the objects list
objects.append(Adapter(korean, speak=korean.speak_korean))
objects.append(Adapter(british, speak=british.speak_english))
for obj in objects:
print("{} says '{}'\n".format(obj.name, obj.speak()))
Korean says 'An-neyong?' British says 'Hello!'
Composite¶
- Useful when data is tree structure, like menu, sub-menu, sub-sub-menu and so on.
- Like building a recursive tree data structure.
- Elements in implementation
- Component - abstract
- Child - concrete, inherits component
- Composite - concrete, inherites component, manages child in tree
class Component(object):
"""Abstract class"""
def __init__(self, *args, **kwargs):
pass
def component_function(self):
pass
class Child(Component): #Inherits from the abstract class, Component
"""Concrete class"""
def __init__(self, *args, **kwargs):
Component.__init__(self, *args, **kwargs)
#This is where we store the name of your child item!
self.name = args[0]
def component_function(self):
#Print the name of your child item here!
print("{}".format(self.name))
class Composite(Component): #Inherits from the abstract class, Component
"""Concrete class and maintains the tree recursive structure"""
def __init__(self, *args, **kwargs):
Component.__init__(self, *args, **kwargs)
#This is where we store the name of the composite object
self.name = args[0]
#This is where we keep our child items
self.children = []
def append_child(self, child):
"""Method to add a new child item"""
self.children.append(child)
def remove_child(self, child):
"""Method to remove a child item"""
self.children.remove(child)
def component_function(self):
#Print the name of the composite object
print("{}".format(self.name))
#Iterate through the child objects and invoke their component function printing their names
for i in self.children:
i.component_function()
#Build a composite submenu 1
sub1 = Composite("submenu1")
#Create a new child sub_submenu 11
sub11 = Child("sub_submenu 11")
#Create a new Child sub_submenu 12
sub12 = Child("sub_submenu 12")
#Add the sub_submenu 11 to submenu 1
sub1.append_child(sub11)
#Add the sub_submenu 12 to submenu 1
sub1.append_child(sub12)
#Build a top-level composite menu
top = Composite("top_menu")
#Build a submenu 2 that is not a composite
sub2 = Child("submenu2")
#Add the composite submenu 1 to the top-level composite menu
top.append_child(sub1)
#Add the plain submenu 2 to the top-level composite menu
top.append_child(sub2)
#Let's test if our Composite pattern works!
top.component_function()
top_menu submenu1 sub_submenu 11 sub_submenu 12 submenu2
Bridge¶
The abstract factory and adapter patterns are the related patterns to the bridge design pattern.
Untangles complicated class hierarchy.
class DrawingAPIOne(object):
"""Implementation-specific abstraction: concrete class one"""
def draw_circle(self, x, y, radius):
print("API 1 drawing a circle at ({}, {} with radius {}!)".format(x, y, radius))
class DrawingAPITwo(object):
"""Implementation-specific abstraction: concrete class two"""
def draw_circle(self, x, y, radius):
print("API 2 drawing a circle at ({}, {} with radius {}!)".format(x, y, radius))
class Circle(object):
"""Implementation-independent abstraction: for example, there could be a rectangle class!"""
def __init__(self, x, y, radius, drawing_api):
"""Initialize the necessary attributes"""
self._x = x
self._y = y
self._radius = radius
self._drawing_api = drawing_api
def draw(self):
"""Implementation-specific abstraction taken care of by another class: DrawingAPI"""
self._drawing_api.draw_circle(self._x, self._y, self._radius)
def scale(self, percent):
"""Implementation-independent"""
self._radius *= percent
#Build the first Circle object using API One
circle1 = Circle(1, 2, 3, DrawingAPIOne())
#Draw a circle
circle1.draw()
#Build the second Circle object using API Two
circle2 = Circle(2, 3, 4, DrawingAPITwo())
#Draw a circle
circle2.draw()
API 1 drawing a circle at (1, 2 with radius 3!) API 2 drawing a circle at (2, 3 with radius 4!)
class Subject(object): #Represents what is being 'observed'
def __init__(self):
self._observers = [] # This where references to all the observers are being kept
# Note that this is a one-to-many relationship: there will be one subject to be observed by multiple _observers
def attach(self, observer):
if observer not in self._observers: #If the observer is not already in the observers list
self._observers.append(observer) # append the observer to the list
def detach(self, observer): #Simply remove the observer
try:
self._observers.remove(observer)
except ValueError:
pass
def notify(self, modifier=None):
for observer in self._observers: # For all the observers in the list
if modifier != observer: # Don't notify the observer who is actually updating the temperature
observer.update(self) # Alert the observers!
class Core(Subject): #Inherits from the Subject class
def __init__(self, name=""):
Subject.__init__(self)
self._name = name #Set the name of the core
self._temp = 0 #Initialize the temperature of the core
@property #Getter that gets the core temperature
def temp(self):
return self._temp
@temp.setter #Setter that sets the core temperature
def temp(self, temp):
self._temp = temp
self.notify() #Notify the observers whenever somebody changes the core temperature
class TempViewer:
def update(self, subject): #Alert method that is invoked when the notify() method in a concrete subject is invoked
print("Temperature Viewer: {} has Temperature {}".format(subject._name, subject._temp))
#Let's create our subjects
c1 = Core("Core 1")
c2 = Core("Core 2")
#Let's create our observers
v1 = TempViewer()
v2 = TempViewer()
#Let's attach our observers to the first core
c1.attach(v1)
c1.attach(v2)
#Let's change the temperature of our first core
c1.temp = 80
c1.temp = 90
Temperature Viewer: Core 1 has Temperature 80 Temperature Viewer: Core 1 has Temperature 80 Temperature Viewer: Core 1 has Temperature 90 Temperature Viewer: Core 1 has Temperature 90
Visitor¶
Lets add new features to existing class hierarchy without changing it.
class House(object): #The class being visited
def accept(self, visitor):
"""Interface to accept a visitor"""
visitor.visit(self) #Triggers the visiting operation!
def work_on_hvac(self, hvac_specialist):
print(self, "worked on by", hvac_specialist) #Note that we now have a reference to the HVAC specialist object in the house object!
def work_on_electricity(self, electrician):
print(self, "worked on by", electrician) #Note that we now have a reference to the electrician object in the house object!
def __str__(self):
"""Simply return the class name when the House object is printed"""
return self.__class__.__name__
class Visitor(object):
"""Abstract visitor"""
def __str__(self):
"""Simply return the class name when the Visitor object is printed"""
return self.__class__.__name__
class HvacSpecialist(Visitor): #Inherits from the parent class, Visitor
"""Concrete visitor: HVAC specialist"""
def visit(self, house):
house.work_on_hvac(self) #Note that the visitor now has a reference to the house object
class Electrician(Visitor): #Inherits from the parent class, Visitor
"""Concrete visitor: electrician"""
def visit(self, house):
house.work_on_electricity(self) #Note that the visitor now has a reference to the house object
#Create an HVAC specialist
hv = HvacSpecialist()
#Create an electrician
e = Electrician()
#Create a house
home = House()
#Let the house accept the HVAC specialist and work on the house by invoking the visit() method
home.accept(hv)
#Let the house accept the electrician and work on the house by invoking the visit() method
home.accept(e)
House worked on by HvacSpecialist House worked on by Electrician
Iterator¶
- The iterator pattern allows a client to have sequential access to the elements of an aggregate object without exposing its underlying structure.
- The composite design pattern is related to the iterator pattern.
def count_to(count):
"""Our iterator implementation"""
#Our list
numbers_in_german = ["eins", "zwei", "drei", "vier", "funf"]
#Our built-in iterator
#Creates a tuple such as (1, "eins")
iterator = zip(range(count), numbers_in_german)
#Iterate through our iterable list
#Extract the German numbers
#Put them in a generator called number
for position, number in iterator:
#Returns a 'generator' containing numbers in German
yield number
#Let's test the generator returned by our iterator
for num in count_to(3):
print("{}".format(num))
print('\n')
for num in count_to(4):
print("{}".format(num))
eins zwei drei eins zwei drei vier
Strategy¶
- It lets change algotithm to a client.
- Useful when object behaviour need to be dynamic.
- A
Strategy
class has default behaviour, and that can be changed dynamically with new one. - Function is swapped using
types
module.
import types #Import the types module
class Strategy:
"""The Strategy Pattern class"""
def __init__(self, function=None):
self.name = "Default Strategy"
#If a reference to a function is provided, replace the execute() method with the given function
if function:
self.execute = types.MethodType(function, self)
def execute(self): #This gets replaced by another version if another strategy is provided.
"""The defaut method that prints the name of the strategy being used"""
print("{} is used!".format(self.name))
#Replacement method 1
def strategy_one(self):
print("{} is used to execute method 1".format(self.name))
#Replacement method 2
def strategy_two(self):
print("{} is used to execute method 2".format(self.name))
#Let's create our default strategy
s0 = Strategy()
#Let's execute our default strategy
s0.execute()
#Let's create the first varition of our default strategy by providing a new behavior
s1 = Strategy(strategy_one)
#Let's set its name
s1.name = "Strategy One"
#Let's execute the strategy
s1.execute()
s2 = Strategy(strategy_two)
s2.name = "Strategy Two"
s2.execute()
Default Strategy is used! Strategy One is used to execute method 1 Strategy Two is used to execute method 2
Chain of Responsibility¶
- Opens various possibility fo a request
- if current handler cannot handle the request, it passess to successor.
- Composite is related to the chain of responsibility, design pattern.
- It is used not to tie a specific solution to a request.
class Handler: #Abstract handler
"""Abstract Handler"""
def __init__(self, successor):
self._successor = successor # Define who is the next handler
def handle(self, request):
handled = self._handle(request) #If handled, stop here
#Otherwise, keep going
if not handled:
self._successor.handle(request)
def _handle(self, request):
raise NotImplementedError('Must provide implementation in subclass!')
class ConcreteHandler1(Handler): # Inherits from the abstract handler
"""Concrete handler 1"""
def _handle(self, request):
if 0 < request <= 10: # Provide a condition for handling
print("Request {} handled in handler 1".format(request))
return True # Indicates that the request has been handled
class DefaultHandler(Handler): # Inherits from the abstract handler
"""Default handler"""
def _handle(self, request):
"""If there is no handler available"""
#No condition checking since this is a default handler
print("End of chain, no handler for {}".format(request))
return True # Indicates that the request has been handled
class Client: # Using handlers
def __init__(self):
self.handler = ConcreteHandler1(DefaultHandler(None)) # Create handlers and use them in a sequence you want
# Note that the default handler has no successor
def delegate(self, requests): # Send your requests one at a time for handlers to handle
for request in requests:
self.handler.handle(request)
# Create a client
c = Client()
# Create requests
requests = [2, 5, 30]
# Send the requests
c.delegate(requests)
Request 2 handled in handler 1 Request 5 handled in handler 1 End of chain, no handler for 30
Design Best Practices¶
- Consistency is the key, use same patther and design solution for different projects
- Identify software architecture that fits you need and keep using it.
- Or use framework, it is pre built with a design pattern to make it flexible, or capable etc. Like flask / Django
- Completeness - build over base of design patter to complete it.
OOPs¶
Class, object, attribute and methods.
__init__
in a class sets its attributes. Takes in param.attribite in init can be with or without underscore. It is naming convention, userscore prefix indicates internal use, it does not mandate.
[?] Implement abstract factory without using inheritance because Python is a dynamically typed language, and therefore does not require abstract classes.