Design patterns are best practices in object oriented programming (OOP) on how to structure our code to solve common problems. It was first proposed in the book Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (Addison-Wesley). Design patterns are basically reusable patterns.
A pattern is a solution to a general design problem in the form of a set of interacting classes. Each pattern describes a problem which occurs over and over again in our environment and then describes the core of the solution to that problem, in such a way that you can use this solution a million times over, without ever doing it the same way twice.
In programming, it is not imperative to use any design pattern. The usage of design pattern is usually done after we refactor and discover that certain parts of our code can be abstracted as the existing design pattern (or the design pattern that you may discover on your own).
In this tutorial, you will learn about:
A singleton is a design pattern for creating only one instance of a class
Purpose: to create only one instance of data
Technique: we check whether an instance is already created. If it is created, we return it. Otherwise, we create a new instance, assign it to a class attribute, and return it.
Applications:
class Singleton(object):
def __new__(cls):
# check whether an instance is already created
if not hasattr(cls, 'instance'):
# create a new instance, assign it to a class attribute
cls.instance = super(Singleton, cls).__new__(cls)
# we return it
return cls.instance
# test-1: s1 is s2 because they are just one object
s1=Singleton()
s2=Singleton()
s1 is s2
True
# test-2: the property of s1 and s2 are the same because they are just one object
s1.message = "I am a singleton"
s2.message
'I am a singleton'
Note, however, that a singleton cannot be inherited. The following example proves this point.
class SingletonChild(Singleton):
pass
c = SingletonChild()
c is s1
True
In contrast, let us define any class which is Non Singleton.
class NonSingleton():
def __init__(self):
pass
# test-1: n1 is NOT n2 because they are not one object
n1=NonSingleton()
n2=NonSingleton()
n1 is n2
False
n1.message = "I am not a singleton"
try:
# test-2: the property of n1 and n2 is NOT the same
n2.message
except Exception as e:
print(e)
'NonSingleton' object has no attribute 'message'
Purpose: to create objects that has the same states including the inheritance
Technique: Python stores the instance state in the dict dictionary and when instantiated normally, every instance will have its own dict. However, in monostate class, we deliberately assign the class variable _shared_state to all of the created instances and then overide the dictionary.
Note that unlike Singleton, all of the instances of Monostate are different objects,but they share the same state.
class Monostate(object):
_shared_state = {}
def __new__(cls, *args, **kwargs):
obj = super(Monostate, cls).__new__(cls, *args, **kwargs)
obj.__dict__ = cls._shared_state
return obj
class MonostateChild(Monostate):
pass
s1=Monostate()
s2=Monostate()
# the two are different objects
s1 is s2
False
# now we define the child object
c1=MonostateChild()
# we set a property to the parent
s1.message = "I am a monostate"
# the child would automatically inherit the parent state
c1.message
'I am a monostate'
Description: The Abstract Factory design pattern is a creational design pattern that provides a way to create objects without specifying the exact class of object that will be created.
Purpose: to provide an interface for creating families of related or dependent objects without specifying their concrete classes.
Technique: This is achieved by defining a common interface for creating objects, and then delegating the task of creating objects to a factory object that implements this interface.
Applications: The Abstract Factory design pattern is commonly used when a system needs to create objects of different types, but the exact type of object to be created is not known until runtime. By using a factory object to create objects, the system can delegate the task of creating objects to the factory object, which can then create the appropriate type of object based on the information it receives at runtime.
See Also: Factory Method
Here’s an example of using a decorator to implement the Abstract Factory design pattern in Python. In this example, the shape_factory function is a decorator that takes a class as an argument and returns a create_shape function. The create_shape function takes a name argument and creates an instance of the decorated class (Shape in this case) with the given name. If an instance with the same name already exists, it returns the existing instance instead of creating a new one.
This is an example of the abstract factory design pattern, where the create_shape function acts as a factory for creating instances of the Shape class.
def shape_factory(cls):
shapes = {}
def create_shape(name):
if name not in shapes:
shapes[name] = cls(name)
return shapes[name]
return create_shape
@shape_factory
class Shape:
def __init__(self, name):
self.name = name
circle = Shape("Circle")
square = Shape("Square")
another_circle = Shape("Circle")
print(circle is square)
print(circle is another_circle)
False True
Decorator adds (wraps) objects to existing objects. Decorator may include one abstract class inheriting another abstract class.
Purpose: to add functionality (to decorate) to existing objects without affecting the other objects
Technique: A Python decorator is a specific change to the Python syntaxfor extending the behavior of a class, method, or function without using inheritance
Applications:
See Also: Python Decorator Tutorial
One problem with computing fibonacci sequence is too long to handle if the number if big. Here is the naive fibonacci function.
def fibo(n):
assert(n >= 0), 'n must be >= 0'
return n if n in (0, 1) else fibo(n-1) + fibo(n-2)
from time import time
start = time()
print(fibo(30))
print(time()-start)
832040 0.24805450439453125
One solution to this problem is to create memoization, that is to return the cache if the argument has been called.
known = {0:0, 1:1}
def fibo(n):
assert(n >= 0), 'n must be >= 0'
if n in known:
# if the argument has been called, use the cache
return known[n]
result = fibo(n-1) + fibo(n-2)
known[n] = result # update the cache
return result
start = time()
print(fibo(30))
print(time()-start)
832040 0.0
One problem of the above solution is reusability. It works for fibonacci but if we want to reuse the memoization for other function, we need to recode from scratch again.
For example, we have naive nsum function or factorial function below.
def firstNsum(n):
'''Returns the sum of the first n numbers'''
assert(n >= 0), 'n must be >= 0'
return 0 if n == 0 else n + firstNsum(n-1)
def fact(n):
assert(n >= 0), 'n must be >= 0'
if (n==0): return 1
result = n * fact(n-1)
return result
If we want to add memoization, we would change the function into the following.
known = {0:0}
def firstNsum(n):
assert(n >= 0), 'n must be >= 0'
if n in known:
return known[n]
result = n + firstNsum(n-1)
known[n] = result
return result
known = {0:1}
def factorial(n):
assert(n >= 0), 'n must be >= 0'
if n in known:
return known[n]
result = n * factorial(n-1)
known[n] = result
return result
Using Python decorator, we can first define the generic memoize function as follow.
import functools
def memoize(fn):
'''
return the cache of any function fn
if the function has been already been called with the same argument
return the cache value instead of calling the function again.
'''
known = dict() # the cache
@functools.wraps(fn)
def memoizer(*args):
if args not in known:
# call the function only if the argument is not known
known[args] = fn(*args)
return known[args]
return memoizer
Then we can use use the naive implementation of the function and add @memoize on top of it. We don't need to change the function for the additional functionality.
@memoize
def fibo(n):
'''Returns the nth number of the Fibonacci sequence'''
assert(n >= 0), 'n must be >= 0'
return n if n in (0, 1) else fibo(n-1) + fibo(n-2)
@memoize
def firstNsum(n):
'''Returns the sum of the first n numbers'''
assert(n >= 0), 'n must be >= 0'
return 0 if n == 0 else n + firstNsum(n-1)
@memoize
def fact(n):
assert(n >= 0), 'n must be >= 0'
if (n==0): return 1
result = n * fact(n-1)
return result
start = time()
print(fibo(30))
print(firstNsum(30))
print(fact(30))
print(time()-start)
832040 465 265252859812191058636308480000000 0.0
A Facade is an abstraction layer implemented over an existing complex system.
Purpose: provides a simplified interface to a library, a framework, or any other complex set of classes.
Technique: to simplify by hiding the internal complexity of our systems and to expose only what is necessary to the client through a simplified interface.
Applications:
Example:
class Waiter:
'''Subsystem # 1'''
def serve(self,food):
print("the waiter is serving", food)
def order(self,food):
print("the waitier is taking order",food,"from client")
def getfood(self,food):
print("the waitier is getting", food,"from kitchen")
class Kitchen:
'''Subsystem # 2'''
def order(self,food):
print("queuing order of ",food, "in kitchen")
class Chefs:
'''Subsystem # 3'''
def cook(self,food):
print("the chef is cooking",food)
class Restaurant:
'''Facade: hide the complexity of sub systems'''
def __init__(self):
self.waiter = Waiter()
self.kitchen = Kitchen()
self.chef = Chefs()
def order(self,*args):
for food in args:
self.waiter.order(food)
for food in args:
self.kitchen.order(food)
self.chef.cook(food)
for food in args:
self.waiter.getfood(food)
self.waiter.serve(food)
if __name__ == "__main__":
""" the client only need to do this simple method of order """
resto1 = Restaurant()
resto1.order("beef steak","salad","omelete")
the waitier is taking order beef steak from client the waitier is taking order salad from client the waitier is taking order omelete from client queuing order of beef steak in kitchen the chef is cooking beef steak queuing order of salad in kitchen the chef is cooking salad queuing order of omelete in kitchen the chef is cooking omelete the waitier is getting beef steak from kitchen the waiter is serving beef steak the waitier is getting salad from kitchen the waiter is serving salad the waitier is getting omelete from kitchen the waiter is serving omelete
Composite Pattern would compose objects into tree structures and then work with these structures as if they were individual objects. One of the main advantages of using the Composite Method is that first, it allows you to compose the objects into the Tree Structure and then work with these structures as an individual object or an entity.
Purpose: to compose objects into Tree type structures to represent the whole-partial hierarchies.
Technique: the composition class would receive the child of the class as component and then set the hierarchical structures.
Applications:
class Shape:
'''
source: https://github.com/tuvo1106/python_design_patterns/blob/master/composite/composite.py
'''
def __init__(self, color=None):
self.color = color
self.children = []
self._name = 'Shape'
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name=value
def _print(self, items, depth):
items.append('> ' * depth)
if self.color:
items.append(self.color)
items.append(f'{self.name}\n')
for child in self.children:
child._print(items, depth + 1)
def __str__(self):
items = []
self._print(items, 0)
return ''.join(items)
class Circle(Shape):
@property
def name(self):
return 'Circle'
class Square(Shape):
@property
def name(self):
return 'Square'
top = Shape()
# top.name = 'Top'
top.children.append(Square('Red'))
top.children.append(Circle('Yellow'))
first = Shape()
first.name = 'First Group'
first.children.append(Circle('Blue'))
first.children.append(Square('Blue'))
top.children.append(first)
subGroup = Shape()
subGroup.name = 'Sub group of the First'
subGroup.children.append(Circle('Yellow'))
subGroup.children.append(Square('Yellow'))
first.children.append(subGroup)
second = Shape()
second.name = 'Second Group'
second.children.append(Circle('Pink'))
second.children.append(Square('Pink'))
top.children.append(second)
print(top)
Shape > RedSquare > YellowCircle > First Group > > BlueCircle > > BlueSquare > > Sub group of the First > > > YellowCircle > > > YellowSquare > Second Group > > PinkCircle > > PinkSquare
the Composite class below is a generic class to define the items in the hierarchy.
class Composite:
'''
Class representing objects at any level of
the hierarchy tree.
Maintains the child objects by adding and
removing them from the tree structure.
'''
def __init__(self, *args):
'''
Takes the first positional argument and
assigns to member variable "position".
Initializes a list of children elements.
'''
self.position = args[0]
self.children = []
def add(self, child):
'''
Adds the supplied child element to the list
of children elements "children".
'''
self.children.append(child)
def remove(self, child):
'''
Removes the supplied child element from
the list of children elements "children".'''
self.children.remove(child)
def _print(self, items, depth):
'''
Prints the details of the component first element
Then, iterates over each of its children.
calling print(top) method.'''
items.append('\t' * depth)
items.append(f'{self.position}\n')
for child in self.children:
child._print(items, depth + 1)
def __str__(self):
'''
String representation of this object
'''
items = []
self._print(items, 0)
return ''.join(items)
# defining the items
top = Composite("Owner")
item1 = Composite("CEO")
item2 = Composite("CFO")
subItem11 = Composite("Chief Secretary")
subItem12 = Composite("Accountant")
subItem21 = Composite("Secretary")
subItem22 = Composite("Bookkeeper")
# set the hierarchy
top.add(item1)
top.add(item2)
item1.add(subItem11)
item2.add(subItem12)
subItem11.add(subItem21)
subItem12.add(subItem22)
print(top)
Owner CEO Chief Secretary Secretary CFO Accountant Bookkeeper
Purpose: to create a bridge between incompatible interfaces.
Technique: to make compatible / standard name of the methods and properties for the incompatible methods or properties.
Applications:
class Adapter:
"""
Adapts an object by replacing methods.
Usage:
motorCycle = MotorCycle()
motorCycle = Adapter(motorCycle, wheels = motorCycle.TwoWheeler)
"""
def __init__(self, obj, **adapted_methods):
"""We set the adapted methods in the object's dict"""
self.obj = obj
self.__dict__.update(adapted_methods)
def __getattr__(self, attr):
"""All non-adapted calls are passed to the object"""
return getattr(self.obj, attr)
def original_dict(self):
"""Print original object dict"""
return self.obj.__dict__
class BeefSteak:
def __init__(self):
self.name = "Steak"
def ingredient(self):
return "beef"
class ChapCai:
def __init__(self):
self.name = "Chap Cai"
def contains(self):
return "mixed vegetables"
class MisoSoup:
def __init__(self):
self.name = "Miso Soup"
def constituent(self):
return "soy bean paste"
"""list to store objects"""
objects = []
maindish = BeefSteak()
objects.append(Adapter(maindish, feature = maindish.ingredient))
vegetable = ChapCai()
objects.append(Adapter(vegetable, feature = vegetable.contains))
soup = MisoSoup()
objects.append(Adapter(soup, feature = soup.constituent))
for obj in objects:
print("The main component of a {0} is {1}".format(obj.name, obj.feature()))
The main component of a Steak is beef The main component of a Chap Cai is mixed vegetables The main component of a Miso Soup is soy bean paste
The Observer pattern describes a publish-subscribe relationship between a publisher (subject or observable), and one or more objects (the subscribers, or observers).
Purpose:
Technique: we have two classes: observer and observable.
Applications:
class Observer:
def __init__(self, observable):
observable.subscribe(self)
self.id=hash(str(self))
def notify(self, observable, *args, **kwargs):
print ("Observer",self.id, 'got', args, kwargs, 'from', observable.id)
class Observable:
def __init__(self):
self._observers = []
self.id="Publisher"
def subscribe(self, observer):
if observer not in self._observers:
self._observers.append(observer)
else:
print('Failed to add: {}'.format(observer))
def notify_observers(self, *args, **kwargs):
for obs in self._observers:
obs.notify(self, *args, **kwargs)
def unsubscribe(self, observer):
try:
self._observers.remove(observer)
except ValueError:
print('Failed to remove: {}'.format(observer))
# Initializing the subject
subject = Observable()
# Initializing two observers with the subject object
observer1 = Observer(subject)
observer2 = Observer(subject)
# The following message will be notified to 2 observers
subject.notify_observers('The 1st broadcast',
kw='ABC News')
subject.unsubscribe(observer2)
# The following message will be notified to just 1 observer since
# the observer has been unsubscribed
subject.notify_observers('The 2nd broadcast',
kw='BCD News')
Observer -7085436326221206853 got ('The 1st broadcast',) {'kw': 'ABC News'} from Publisher Observer 2475745222156064036 got ('The 1st broadcast',) {'kw': 'ABC News'} from Publisher Observer -7085436326221206853 got ('The 2nd broadcast',) {'kw': 'BCD News'} from Publisher
Last Updated: 04 June 2023