What is design pattern?¶

by Kardi Teknomo

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:

  • Singleton Pattern
  • Monostate Pattern
  • Abstract Factory Pattern
  • Decorator Pattern
  • Facade Pattern
  • Composite Pattern
  • Adapter Pattern
  • Observer Pattern

Singleton Pattern¶

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:

  • when you need to have only one object
  • when you need a global point of access for the resource from multiple or different parts of the system (e.g. managing a connection to a database, logging class to send messages to log, retrieving and storing information on configuration files)
  • read-only singletons storing some global states (user language, time, time zone, application path, etc.)
In [1]:
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 
In [2]:
# test-1: s1 is s2 because they are just one object
s1=Singleton()
s2=Singleton()
s1 is s2
Out[2]:
True
In [3]:
# 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
Out[3]:
'I am a singleton'

Note, however, that a singleton cannot be inherited. The following example proves this point.

In [4]:
class SingletonChild(Singleton):
    pass

c = SingletonChild()
c is s1
Out[4]:
True

In contrast, let us define any class which is Non Singleton.

In [5]:
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
Out[5]:
False
In [6]:
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'

Monostate Pattern¶

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.

In [7]:
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
In [8]:
s1=Monostate()
s2=Monostate()

# the two are different objects
s1 is s2
Out[8]:
False
In [9]:
# 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
Out[9]:
'I am a monostate'

Abstract Factory Pattern¶

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

shape_factory¶

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.

In [10]:
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
In [11]:
circle = Shape("Circle")
square = Shape("Square")
another_circle = Shape("Circle")

print(circle is square) 
print(circle is another_circle) 
False
True

Decorator Pattern¶

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:

  • you want to maintain the original functionality while adding new functionality
  • you want to deliver both the core functionality and then “decorate” those cores with functionality unique to the customer
  • to extend the functionality of an object

See Also: Python Decorator Tutorial

Example: Fibonacci and Factorial¶

One problem with computing fibonacci sequence is too long to handle if the number if big. Here is the naive fibonacci function.

In [12]:
def fibo(n):
    assert(n >= 0), 'n must be >= 0'
    return n if n in (0, 1) else fibo(n-1) + fibo(n-2)
In [13]:
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.

In [14]:
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
In [15]:
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.

In [16]:
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)
In [17]:
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.

In [18]:
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
In [19]:
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.

In [20]:
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.

In [21]:
@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
In [22]:
start = time()
print(fibo(30))
print(firstNsum(30))
print(fact(30))
print(time()-start)
832040
465
265252859812191058636308480000000
0.0

Facade Pattern¶

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:

  • when we want to provide a simple interface to client code that wants to use a complex system but does not need to be aware of the system's complexity
  • when you need to have a limited but straightforward interface to a complex subsystem.
  • when you want to structure a subsystem into layers with facades to define entry points to each level of a subsystem.

Example:

  • Instead of dealing with complex video conversion (which may include audio and video file handling, bit rate, codec, compression, etc), the facade Video conversion class contains simplified method convertVideo(filename,format).
  • Instead of dealing with so many products and departments, the facade shop provides teh clients with a simple voice interface to the ordering system, payment gateways, and various delivery services.
In [23]:
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¶

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:

  • to create a hierarchical structure of objects while treating individual objects and compositions of objects uniformly.
In [24]:
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'
In [25]:
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.

In [26]:
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)
In [27]:
# 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

Adapter Pattern¶

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:

  • when you want to deal with different data format (such as XML, JSON, Database)
  • when you are dealing with different name of methods of the same group
In [28]:
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__
In [29]:
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"
In [30]:
"""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

Observer Pattern¶

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:

  • sending notifications to many objects
  • one-to-many relationship is defined between the objects
  • when an object is altered, it will lead to a cascade of changes to be applied to the dependent objects
In [31]:
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)) 
In [32]:
# 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

References¶

  • https://refactoring.guru/design-patterns/facade
  • https://github.com/tuvo1106/python_design_patterns
  • https://hackernoon.com/the-ultimate-guide-to-design-patterns-and-generic-composite-in-python-cc1x339j
  • https://stackify.com/solid-design-principles/
  • https://www.geeksforgeeks.org/factory-method-python-design-patterns/

Last Updated: 04 June 2023