Python - Creational Design Patterns

Python - Creational Design Patterns

Introduction

Design patterns offer solutions to common software design problems. When it comes to OOP (object-oriented programming), design patterns are more focused on solving the problems of object generation and interaction, rather than the larger scale problems of overall software architecture.[1]

Gang of Four

In 1994, four authors -- Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides (pictured below) -- also known as the Gang of Four (GoF) released the hugely popular - Design Patterns: Elements of Reusable Object-Oriented Software. This book contained various techniques around software design, including twenty-three software design patterns, categorized into three types: creational, structural, and behavioral.

Within this article, we will cover the Creational Patterns based upon the Python language.

image2-1
Figure 1: GoF

What is a Creational Pattern?

As per -- Elements of Reusable Object-Oriented Software -- creational design patterns are,

... composed of two dominant ideas. One is encapsulating knowledge about which concrete classes the system uses. The other is hiding how instances of these concrete classes are created and combined.

Factory Pattern

The Factory pattern is a design pattern that allows you to use a function or method (known as the factory method) to handle the creation of an object.

In our example below, we can see that the method get_router is our factory method. Therefore we only need to interact with the factory method for the creation of our classes.

class JuniperRouter:
    def __init__(self, device_id):
        self._device_id = device_id

    def model_name(self):
        return "MX"

class CiscoRouter:
    def __init__(self, device_id):
        self._device_id = device_id

    def model_name(self):
        return "ASR"

def get_router(router="cisco"):
    """The factory method"""
    routers = dict(juniper=JuniperRouter("011111"),
                   cisco=CiscoRouter("022222"))
    return routers[router]
... c = get_router("cisco")
... print(c.model_name())
ASR
...
... j = get_router("juniper")
... print(j.model_name())
MX

Abstract Factory Pattern

The Abstract Factory pattern is also called a factory of factories.

With the Factory pattern, previously seen, you produce instances of a class via a factory method. With the Abstract Factory pattern, you provide a way for anyone to provide their own factory.

The best way to explain this is via an example. Our example is based upon a factory that manufactures copper cabling. In the world of Python, this is the CopperFactory class and the class that it returns - Copper. In addition, we also have a cable shop that retails our cables. However, within this cable shop i.e the CableShop class, we implement an Abstract Factory. This takes in our CopperFactory, as shown in the code below.

class Copper:
    def get_spec(self):
        return "cat5e"

    def __str__(self):
        return "Copper"

class CopperFactory:
    """Concrete Factory"""

    def get_cable(self):
        return Copper()

    def get_cost(self):
        return "$50"

class CableShop:
    """ CableShop uses our Abstract Factory """

    def __init__(self, cable_factory=None):
        """ cable_factory is our Abstract Factory """
        self._cable_factory = cable_factory

    def show_cable(self):
        """ Utility method to display the details of the objects returned by the concrete Factory """
        cable = self._cable_factory.get_cable()
        cable_cost = self._cable_factory.get_cost()
        print("Cable manufactured is '{}'!".format(cable))
        print("Cable spec is '{}'".format(cable.get_spec()))
        print("Cable cost is '{}' per unit".format(cable_cost))
... #Create a Concrete Factory for Copper Factory
... copper_factory = CopperFactory()
... 
... #Create a cable shop containing our Abstract Factory
... cable_shop = CableShop(copper_factory)
... cable_shop.show_cable()
... 
Cable manufactured is 'Copper'!
Cable spec is 'cat5e'
Cable cost is '$50' per unit

The company has expanded and a new Factory is required for fiber cabling. Therefore, we create Fiber and FiberFactory. We can now pass this into CableShop, without requiring any change to the Abstract Factory and the methods previously called i.e .show.cable()

class Fiber:
 	def get_spec(self):
 		return "single-mode"
 
 	def __str__(self):
 		return "Fiber"
 
class FiberFactory:
 	"""Concrete Factory"""
  	def get_cable(self):
 		return Fiber()
 
 	def get_cost(self):
 		return "$80"
... #Create a Concrete Factory for Fiber Factory
... fiber_factory = FiberFactory()
... 
... #Create a pet store housing our Abstract Factory
... cable_shop = CableShop(fiber_factory)
... cable_shop.show_cable()
Cable manufactured is 'Fiber'!
Cable spec is 'single-mode'
Cable cost is '$80' per unit

Builder Pattern

The main advantages of the Builder pattern are that it provides a clear separation between the construction and representation of an object, and in turn, provides better control over the construction process.[2]

The pattern consists of two main participants: the builder and the director. The builder is responsible for creating the various parts of the complex object. The director controls the building process using a builder instance.[3]

Below shows a clear representation of the various elements[4] based on a restaurant scenario. Makes sense, right? Why would the client care about how the meal is built. I mean, they just want the meal! This is exactly what the Builder pattern provides.

image1-1
Figure 2: Builder pattern example.

In our example below, we can see that the HardwareEngineer is the director. Which instructs the builder, ComputerBuilder to build the computer i.e instantiate the objects.

class Computer:
    def __init__(self, serial_number):
        self.serial = serial_number
        self.memory = None      # in gigabytes
        self.hdd = None         # in gigabytes
        self.gpu = None

    def __str__(self):
        info = ('Memory: {}GB'.format(self.memory),
                'Hard Disk: {}GB'.format(self.hdd),
                'Graphics Card: {}'.format(self.gpu))
        return '\n'.join(info)

# builder
class ComputerBuilder:
    def __init__(self):
        self.computer = Computer('AG23385193')

    def configure_memory(self, amount):
        self.computer.memory = amount

    def configure_hdd(self, amount):
        self.computer.hdd = amount

    def configure_gpu(self, gpu_model):
        self.computer.gpu = gpu_model

# director
class HardwareEngineer:
    def __init__(self):
        self.builder = None

    def construct_computer(self, memory, hdd, gpu):
        self.builder = ComputerBuilder()
        [step for step in (self.builder.configure_memory(memory),
                           self.builder.configure_hdd(hdd),
                           self.builder.configure_gpu(gpu))]

    @property
    def computer(self):
        return self.builder.computer
... director = HardwareEngineer()
... director.construct_computer(hdd=500, memory=9, gpu='GeForce GTX 650 Ti')
... computer = director.computer
... print(computer)
Memory: 9GB
Hard Disk: 500GB
Graphics Card: GeForce GTX 650 Ti

Singleton Pattern

A Singleton only allows one, and one object only to be instantiated from a class.

This can be useful in instances where you want to provide, for example, a global configuration object.

Below shows an example, which is based on the previously mentioned configuration object scenario. As you can see even though we try to instantiate a new copy of the class, we receive the same one, as per the class id() shown.

class Singleton(object):
    def __new__(cls):
        if not hasattr(cls, 'instance') or not cls.instance:
            cls.instance = super().__new__(cls)
        return cls.instance
... # singleton for app1
... configuration_for_app1 = Singleton()
... configuration_for_app1.db_username = 'Cyril'
... 
... print("db_username = {}".format(configuration_for_app1.db_username))
... 
... # singleton for app2
... configuration_for_app2 = Singleton()
... print("db_username = {}".format(configuration_for_app2.db_username))
... 
... # compare singleton class id's
... print(id(configuration_for_app1))
... print(id(configuration_for_app2))
db_username = Cyril
db_username = Cyril
140484363288928
140484363288928

Prototype Pattern

The Prototype pattern is used when the object creation is a costly affair and requires a lot of time and resources, and you have a similar object already existing. This pattern provides a mechanism to copy the original object to a new object and then modify it according to our needs.[5]

Below shows an example where the creation of a new object would require a huge SQL call. We create a Prototype class. This contains the required methods to clone our classes, as shown below with the cloning of fw1 to fw2

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 Firewall:
    def __init__(self):
        self.throughput = "10Gb"
        self.max_conn = "50k"
        self.max_vpn = "100"
        print("WARNING! HUGE SQL query made to CMDB ...")

    def __str__(self):
        return '{} | {} | {}'.format(self.throughput,
                                     self.max_conn,
                                     self.max_vpn)
... # create original object
... fw1 = Firewall()
... print("original object = {}".format(fw1))
... 
... # clone
... prototype = Prototype()
... prototype.register_object('srx', fw1)
... fw2 = prototype.clone('srx')
... 
... # print clone
... print("cloned object = {}".format(fw2))
WARNING! HUGE SQL query made to CMDB ...
original object = 10Gb | 50k | 100
cloned object = 10Gb | 50k | 100

References


  1. "2. The Builder Pattern - Mastering Python Design ... - O'Reilly Media." https://www.oreilly.com/library/view/mastering-python-design/9781783989324/ch02.html. Accessed 17 Apr. 2019. ↩︎

  2. "2. The Builder Pattern - Mastering Python Design ... - O'Reilly Media." https://www.oreilly.com/library/view/mastering-python-design/9781783989324/ch02.html. Accessed 17 Apr. 2019. ↩︎

  3. "2. The Builder Pattern - Mastering Python Design ... - O'Reilly Media." https://www.oreilly.com/library/view/mastering-python-design/9781783989324/ch02.html. Accessed 17 Apr. 2019. ↩︎

  4. "2. The Builder Pattern - Mastering Python Design ... - O'Reilly Media." https://www.oreilly.com/library/view/mastering-python-design/9781783989324/ch02.html. Accessed 17 Apr. 2019. ↩︎

  5. "Prototype design pattern in Java and Python | en.proft.me." 27 Sep. 2016, https://en.proft.me/2016/09/27/prototype-design-pattern-java-and-python/. Accessed 17 Apr. 2019. ↩︎

Subscribe to our newsletter and stay updated.

Don't miss anything. Get all the latest posts delivered straight to your inbox.
Great! Check your inbox and click the link to confirm your subscription.
Error! Please enter a valid email address!