Patterns in Python – creational

In this section, we will take a look at a few of the common creational patterns. We will start with Singleton, and then go on to Prototype, Builder, and Factory, in that order.

The Singleton pattern

The Singleton pattern is one of the most well-known and easily understood patterns in the entire pantheon of design patterns. It is usually defined as:

A Singleton is a class which has only one instance and a well-defined point of access to it.

The requirements of a Singleton can be summarized as follows:

  • A class must have only one instance accessible via a well-known access point.
  • The class must be extensible by inheritance without breaking the pattern.
  • The simplest Singleton implementation in Python is shown next. It is done by overriding the __new__ method of the base object type:
    # singleton.py
    class Singleton(object):
        """ Singleton in Python """
        
        _instance = None
        
        def __new__(cls):
            if cls._instance == None:
                cls._instance = object.__new__(cls)
            return cls._instance
    >>> from singleton import Singleton
    >>> s1 = Singleton()
    >>> s2 = Singleton()
    >>> s1==s2
    True
    
  • Since we would be requiring this check for a while, let's define a function for the same:
    def test_single(cls):
        """ Test if passed class is a singleton """
        return cls() == cls()
  • Now let's see if our Singleton implementation satisfies the second requirement. We will define a simple subclass to test this:
    class SingletonA(Singleton):
        pass
    
    >>> test_single(SingletonA)
    True

Cool! So our simple implementation passes the test. Are we done here now?

Well, the point with Python, as we discussed before, is that it provides a number of ways to implement patterns due to its dynamism and flexibility. So, let's stay with with Singleton for a while, and see if we can get some illustrative examples which would give us insights into the power of Python:

class MetaSingleton(type):
    """ A type for Singleton classes (overrides __call__) """    

    def __init__(cls, *args):
        print(cls,"__init__ method called with args", args)
        type.__init__(cls, *args)
        cls.instance = None

    def __call__(cls, *args, **kwargs):
        if not cls.instance:
            print(cls,"creating instance", args, kwargs)
            cls.instance = type.__call__(cls, *args, **kwargs)
        return cls.instance

class SingletonM(metaclass=MetaSingleton):
    pass

The preceding implementation moves the logic of creating a Singleton to the type of the class, namely, its metaclass.

We first create a type for Singletons, named MetaSingleton, by extending the type and overriding the __init__ and __call__ methods on the metaclass. Then we declare that the SingletonM class, SingletonM, uses the metaclass.

>>> from singleton import *
<class 'singleton.SingletonM'> __init__ method called with args ('SingletonM', (), {'__module__': 'singleton', '__qualname__': 'SingletonM'})
>>> test_single(SingletonM)
<class 'singleton.SingletonM'> creating instance ()
True

The following is a peep into what is happening behind the scenes in the new implementation of the Singleton:

  • Initializing a class variable: We can either do it at the class level (just after the class declaration) as we saw in the previous implementation, or we can put it in the metaclass __init__ method. This is what we are doing here for the _instance class variable, which will hold the single instance of the class.
  • Overriding class creation: One can either do it at the class level by overriding the __new__ method of class as we saw in previous implementation, or, equivalently, we can do it in the metaclass by overriding its __call__ method. This is what the new implementation does.

Note

When we override a class's __call__ method, it affects its instance, and instances become callable. Similarly, when we override a metaclass's _call_ method, it affects its classes, and modifies the way the classes are called—in other words, the way the class creates its instances.

Let's take a look at the pros and cons in the metaclass approach over the class approach:

  • One benefit is that we can create any number of new top-level classes which get the Singleton behavior via the metaclass. Using the default implementation, every class has to inherit the top-level class Singleton or its subclasses to obtain the Singleton behavior. The metaclass approach provides more flexibility with respect to class hierarchies.
  • However, the metaclass approach can be interpreted as creating slightly obscure and difficult-to-maintain code as opposed to the class approach. This is because fewer Python programmers understand metaclasses and metaprogramming when compared to those who understand classes. This may be a disadvantage with the metaclass solution.

Now let's think out of the box, and see if we can solve the Singleton problem in a slightly different way.

The Singleton – do we need a Singleton?

Let's paraphrase the first requirement of a Singleton in a slightly different way:

A class must provide a way for all its instances to share the same initial state.

To explain that, let's briefly look at what a Singleton pattern actually tries to achieve.

When a Singleton ensures it has only one instance, what it guarantees is that the class provides one single state when it is created and initialized. In other words, what a Singleton actually gives is a way for a class to ensure a single shared state across all its instances.

In other words, the first requirement of the Singleton pattern can be paraphrased in a slightly different form, which has the same end result as the first form.

A class must provide a way for all its instances to share the same initial state.

The technique of ensuring just a single actual instance at a specific memory location is just one way of achieving this.

Ah! So what has been happening so far is that we have been expressing the pattern in terms of the implementation details of less flexible and versatile programming languages. With a language such as Python, we need not stick pedantically to this original definition.

Let's look at the following class:

class Borg(object):
    """ I am not a Singleton """

    __shared_state = {}
    def __init__(self):
        self.__dict__ = self.__shared_state

This pattern ensures that when you create a class, you specifically initialize all of its instances with a shared state which belongs to the class (since it is declared at the class level).

What we really care about in a Singleton is actually this shared state, so Borg works without worrying about all instances being exactly the same.

Since this is Python, it does this by initializing a shared state dictionary on the class, and then instantiating the instance's dictionary to this value, thereby ensuring that all instances share the same state.

The following is a specific example of Borg in action:

class IBorg(Borg):
    """ I am a Borg """
    
    def __init__(self):
        Borg.__init__(self)
        self.state = 'init'

    def __str__(self):
        return self.state

>>> i1 = IBorg()
>>> i2 = IBorg()
>>> print(i1)
init
>>> print(i2)
init
>>> i1.state='running'
>>> print(i2)
running
>>> print(i1)
running
>>> i1==i2
False

By using Borg, we managed to create a class whose instances share the same state, even though the instances are actually not the same. And the state change was propagated across the instances; as the preceding example shows, when we change the value of state in i1, it also changes in i2.

What about dynamic values? We know they will work in a Singleton, since it's the same object always, but what about the Borg?

>>> i1.x='test'
>>> i2.x
'test'

So we attached a dynamic attribute x to instance i1, and it appeared in instance i2 as well. Neat!

So let's see if Borg offers any benefits over Singleton:

  • In a complex system where we may have multiple classes inheriting from a root Singleton class, it may be difficult to impose the requirement of a single instance due to import issues or race conditions—for example, if a system is using threads. The Borg pattern circumvents these problems neatly by doing away with the requirement for a single instance in memory.
  • The Borg pattern also allows for simple sharing of state across the Borg class and all its subclasses. This is not the case for a Singleton, since each subclass creates its own state. We will see an example illustrating this next.

State sharing – Borg versus Singleton

A Borg pattern always shares the same state from the top class (Borg) down to all the subclasses. This is not the case with a Singleton. Let's see an illustration.

For this exercise, we will create two subclasses of our original Singleton class, namely, SingletonA and SingletonB:

>>> class SingletonA(Singleton): pass
... 
>>> class SingletonB(Singleton): pass
... 

Let's create a subclass of SingletonA, namely, SingletonA1:

>>> class SingletonA1(SingletonA): pass
...

Now let's create instances:

>>> a = SingletonA()
>>> a1 = SingletonA1()
>>> b = SingletonB()

Let's attach a dynamic property, x, with a value 100 to a:

>>> a.x = 100
>>> print(a.x)
100

Let's check if this is available on the a1 instance of the SingletonA1 subclass:

>>> a1.x
100

Good! Now let's check if it is available on the b instance:

>>> b.x
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'SingletonB' object has no attribute 'x'

Oops! So, it appears that SingletonA and SingletonB don't share the same state. This is why a dynamic attribute that is attached to an instance of SingletonA appears in the instance of its sub-classes, but doesn't appear on the instance of a sibling or peer subclass namely SingletonB – because it is a different branch of the class hierarchy from the top-level Singleton class.

Let's see if Borgs can do any better.

First, let's create the classes and their instances:

>>> class ABorg(Borg):pass
... 
>>> class BBorg(Borg):pass
... 
>>> class A1Borg(ABorg):pass
... 
>>> a = ABorg()
>>> a1 = A1Borg()
>>> b = BBorg()

Now let's attach a dynamic attribute x to a with value 100:

>>> a.x = 100
>>> a.x
100
>>> a1.x
100

Let's check if the instance of the sibling class Borg also gets it:

>>> b.x
100

This proves that the Borg pattern is much better at state sharing across classes and sub classes than the Singleton pattern, and it does so without a lot of fuss or the overhead of ensuring a single instance.

Let's now move on to other creational patterns.

The Factory pattern

The Factory pattern solves the problem of creating instances of related classes to another class, which usually implements instance creation via a single method, usually defined on a parent Factory class and overridden by subclasses (as needed).

The Factory pattern provides a convenient way for the client (user) of a class to provide a single entry point to create instances of classes and subclasses, usually, by passing in parameters to a specific method of the Factory class: the factory method.

Let's look at a specific example:

from abc import ABCMeta, abstractmethod

class Employee(metaclass=ABCMeta):
    """ An Employee class """

    def __init__(self, name, age, gender):
        self.name = name
        self.age = age
        self.gender = gender

    @abstractmethod
    def get_role(self):
        pass
    
    def __str__(self):
        return "{} - {}, {} years old {}".format(self.__class__.__name__,
                                                 self.name,
                                                 self.age,
                                                 self.gender)

class Engineer(Employee):
    """ An Engineer Employee """
    
    def get_role(self):
        return "engineering"

class Accountant(Employee):
    """ An Accountant Employee """
    
    def get_role(self):
        return "accountant" 

class Admin(Employee):
    """ An Admin Employee """

    def get_role(self):
        return "administration"

We have created a general Employee class with some attributes and three subclasses, namely, Engineer, Accountant, and Admin.

Since all of them are related classes, a Factory class is useful to abstract away the creation of instances of these classes.

The following is our EmployeeFactory class:

class EmployeeFactory(object):
    """ An Employee factory class """

    @classmethod
    def create(cls, name, *args):
        """ Factory method for creating an Employee instance """

        name = name.lower().strip()
        
        if name == 'engineer':
            return Engineer(*args)
        elif name == 'accountant':
            return Accountant(*args)
        elif name == 'admin':
            return Admin(*args)

The class provides a single create factory method that accepts a name parameter, which is matched to the class's name and instance created accordingly. The rest of the arguments are parameters required for instantiating the class's instance, which is passed unchanged to its constructor.

Let's see our Factory class in action:

>>> factory = EmployeeFactory()
>>> print(factory.create('engineer','Sam',25,'M'))
Engineer - Sam, 25 years old M
>>> print(factory.create('engineer','Tracy',28,'F'))
Engineer - Tracy, 28 years old F

>>> accountant = factory.create('accountant','Hema',39,'F')
>>> print(accountant)

Accountant - Hema, 39 years old F
>>> accountant.get_role()

accounting
>>> admin = factory.create('Admin','Supritha',32,'F')
>>> admin.get_role()
'administration'

The following are a few interesting notes about our Factory class:

  • A single factory class can create instances of any class in the Employee hierarchy.
  • In the Factory pattern, it is conventional to use one Factory class associated to a class family (a class and its subclass hierarchy). For example, a Person class could use a PersonFactory, an automobile class could use AutomobileFactory, and so on.
  • The factory method is usually decorated as a classmethod in Python. This way it can be called directly via the class namespace. For example:
        >>> print(EmployeeFactory.create('engineer','Vishal',24,'M'))
        Engineer - Vishal, 24 years old M

In other words, an instance of the Factory class is really not required for this pattern.

The Prototype pattern

The Prototype design pattern allows a programmer to create an instance of a class as a template instance, and then create new instances by copying or cloning this Prototype.

A Prototype is most useful in the following cases:

  • When the classes instantiated in a system are dynamic, that is, they are specified as part of a configuration, or can otherwise change at runtime.
  • When the instances only have a few combinations of initial state. Rather than keeping track of the state and instantiating an instance each time, it is more convenient to create prototypes matching each state and clone them.

A Prototype object usually supports copying itself via the clone method.

The following is a simple implementation of the Prototype in Python:

import copy

class Prototype(object):
    """ A prototype base class """

    def clone(self):
        """ Return a clone of self """
        return copy.deepcopy(self)

The clone method is implemented using the copy module, which performs a deepcopy on the object? and returns a clone.

Let's see how this works. For that, we need to create a meaningful subclass:

class Register(Prototype):
    """ A student Register class  """
    
    def __init__(self, names=[]):
        self.names = names


>>> r1=Register(names=['amy','stu','jack'])
>>> r2=r1.clone()
>>> print(r1)
<prototype.Register object at 0x7f42894e0128>
>>> print(r2)
<prototype.Register object at 0x7f428b7b89b0>


>>> r2.__class__
<class 'prototype.Register'>

Prototype – deep versus shallow copy

Now let's take a deeper look at the implementation details of our Prototype class.

You may notice that we use the deepcopy method of the copy module to implement our object cloning. This module also has a copy method, which implements shallow copying.

If you implement shallow copying, you will find that all objects are copied via a reference. This is fine for immutable objects such as strings or tuples, as they can't be changed.

However, for mutables such as lists or dictionaries, this is a problem since the state of the instance is shared instead of being wholly owned by the instance, and any modification of a mutable in one instance will modify the same object in the cloned instances as well!

Let's see an example. We will use a modified implementation of our Prototype class, which uses shallow copying, to demonstrate this:

class SPrototype(object):
    """ A prototype base class using shallow copy """

    def clone(self):
        """ Return a clone of self """
        return copy.copy(self)

The SRegister class inherits from the new prototype class:

class SRegister(SPrototype):
    """ Sub-class of SPrototype """
    
    def __init__(self, names=[]):
        self.names = names


>>> r1=SRegister(names=['amy','stu','jack'])
>>> r2=r1.clone()

Let's add a name to the names register of instance r1:

>>> r1.names.append('bob')

Now let's check r2.names:

>>> r2.names
['amy', 'stu', 'jack', 'bob']

Oops! This is not what we wanted, but due to the shallow copy, both r1 and r2 end up sharing the same names list, as only the reference is copied over, not the entire object. This can be verified by a simple inspection:

>>> r1.names is r2.names
True

A deep copy, on the other hand, calls copy recursively for all objects contained in the cloned (copied) object, so nothing is shared, but each clone will end up having its own copy of all the referenced objects.

Prototype using metaclasses

We've seen how to build the Prototype pattern using classes. Since we've already seen a bit of meta-programming in Python in the Singleton pattern example, let's find out whether we can do the same in Prototype.

What we need to do is attach a clone method to all the Prototype classes. Dynamically attaching a method to a class like this can be done in its metaclass via the __init__ method of the metaclass.

This provides a simple implementation of Prototype using metaclasses:

import copy

class MetaPrototype(type):

    """ A metaclass for Prototypes """

    def __init__(cls, *args):
        type.__init__(cls, *args)
        cls.clone = lambda self: copy.deepcopy(self) 

class PrototypeM(metaclass=MetaPrototype):
    pass

The PrototypeM class now implements a Prototype pattern. Let's see an illustration by using a subclass:

class ItemCollection(PrototypeM):
    """ An item collection class """

    def __init__(self, items=[]):
        self.items = items

First we create an ItemCollection object:

>>> i1=ItemCollection(items=['apples','grapes','oranges'])
>>> i1
<prototype.ItemCollection object at 0x7fd4ba6d3da0>

Now we clone it as follows:

>>> i2 = i1.clone()

The clone is clearly a different object:

>>> i2
<prototype.ItemCollection object at 0x7fd4ba6aceb8>

And it has its own copy of the attributes:

>>> i2.items is i1.items
False

Combining patterns using metaclasses

It is possible to create interesting and customized patterns by using the power of metaclasses. The following example illustrates a type which is both a Singleton as well as a Prototype:

class MetaSingletonPrototype(type):
    """ A metaclass for Singleton & Prototype patterns """

    def __init__(cls, *args):
        print(cls,"__init__ method called with args", args)
        type.__init__(cls, *args)
        cls.instance = None
        cls.clone = lambda self: copy.deepcopy(cls.instance)

    def __call__(cls, *args, **kwargs):
        if not cls.instance:
            print(cls,"creating prototypical instance", args, kwargs)
            cls.instance = type.__call__(cls,*args, **kwargs)
        return cls.instance

Any class using this metaclass as its type would show both Singleton and Prototype behavior.

It may look a bit strange to have a single class combine what look like conflicting behaviors into one, since a Singleton allows only one instance and a Prototype allows cloning to derive multiple instances, but if we think of patterns in terms of their APIs then it begins to feel a bit more natural:

  • Calling the class using the constructor would always return the same instance – it behaves like the Singleton pattern.
  • Calling clone on the class's instance would always return cloned instances. The instances are always cloned using the Singleton instance as the source – it behaves like the Prototype pattern.

Here, we have modified our PrototypeM class to now use the new metaclass:

class PrototypeM(metaclass=MetaSingletonPrototype):
    pass

Since ItemCollection continues to subclass PrototypeM, it automatically gets the new behavior.

Take a look at the following code:

>>> i1=ItemCollection(items=['apples','grapes','oranges'])
<class 'prototype.ItemCollection'> creating prototypical instance () {'items': ['apples'
, 'grapes', 'oranges']}
>>> i1
<prototype.ItemCollection object at 0x7fbfc033b048>
>>> i2=i1.clone()

The clone method works as expected, and produces a clone:

>>> i2
<prototype.ItemCollection object at 0x7fbfc033b080>
>>> i2.items is i1.items
False

However, building an instance via the constructor always returns the Singleton (Prototype) instance only as it invokes the Singleton API:

>>> i3=ItemCollection(items=['apples','grapes','mangoes'])
>>> i3 is i1
True

Metaclasses allow powerful customization of class creation. In this specific example, we created a combination of behaviors which included both Singleton and Prototype patterns into one class via a metaclass. The power of Python using metaclasses allows the programmer to go beyond traditional patterns and come up with creative techniques.

The Prototype factory

A prototype class can be enhanced with a helper Prototype factory or registry class, which can provide factory functions for creating prototypical instances of a configured family or group of products. Think of this as a variation on our previous Factory pattern.

The following is the code for this class. Notice that we inherit it from Borg to share state automatically from the top of the hierarchy:

class PrototypeFactory(Borg):
    """ A Prototype factory/registry class """
    
    def __init__(self):
        """ Initializer """

        self._registry = {}

    def register(self, instance):
        """ Register a given instance """

        self._registry[instance.__class__] = instance

    def clone(self, klass):
        """  Return cloned instance of given class """

        instance = self._registry.get(klass)
        if instance == None:
            print('Error:',klass,'not registered')
        else:
            return instance.clone()

Let's create a few subclasses of Prototype, whose instances we can register on the factory:

class Name(SPrototype):
    """ A class representing a person's name """
    
    def __init__(self, first, second):
        self.first = first
        self.second = second

    def __str__(self):
        return ' '.join((self.first, self.second))
                             

class Animal(SPrototype):
    """ A class representing an animal """

    def __init__(self, name, type='Wild'):
        self.name = name
        self.type = type

    def __str__(self):
        return ' '.join((str(self.type), self.name))

We have two classes: one, a Name class another, an animal class, both of which inherit from SPrototype.

First create a name and animal object:

>>> name = Name('Bill', 'Bryson')
>>> animal = Animal('Elephant')
>>> print(name)
Bill Bryson
>>> print(animal)
Wild Elephant

Now, let's create an instance of PrototypeFactory:

>>> factory = PrototypeFactory()

Now let's register the two instances on the factory:

>>> factory.register(animal)
>>> factory.register(name)

Now the factory is ready to clone any number of instances from the configured instances:

>>> factory.clone(Name)
<prototype.Name object at 0x7ffb552f9c50>

>> factory.clone(Animal)
<prototype.Animal object at 0x7ffb55321a58>

The factory, rightfully, complains if we try to clone a class whose instance is not registered:

>>> class C(object): pass
... 
>>> factory.clone(C)
Error: <class '__main__.C'> not registered

Note

The factory class shown here could be enhanced with a check for the existence of the clone method on the registered class to make sure any class that is registered is obeying the API of the Prototype class. This is left as an exercise to the reader.

It is instructive to discuss a few aspects of the specific example we have chosen if the reader hasn't observed them already:

  • The PrototypeFactory class is a Factory class, so it is usually a Singleton. In this case, we have made it a Borg, as we've seen that Borgs make a better fist of state sharing across class hierarchies.
  • The Name class and Animal class inherit from SPrototype, since their attributes are integers and strings which are immutable; so, a shallow copy is fine here. This is unlike our first Prototype subclass.
  • Prototypes preserve the class creation signature in the prototypical instance, namely the clone method. This makes it easy for the programmer, as he/she does not to have to worry about the class creation signature, the order and type of parameters to __new__, and hence, the __init__ methods, but only has to call clone on an existing instance.

The Builder pattern

A Builder pattern separates out the construction of an object from its representation (assembly) so that the same construction process can be used to build different representations.

In other words, using a Builder pattern one can conveniently create different types or representative instances of the same class, each using a slightly different building or assembling process.

Formally, the Builder pattern uses a Director class, which instructs the Builder object to build instances of the target class. Different types (classes) of builders help to build slightly different variations on the same class.

Let's look at an example:

class Room(object):
    """ A class representing a Room in a house """
    
    def __init__(self, nwindows=2, doors=1, direction='S'):
        self.nwindows = nwindows
        self.doors = doors
        self.direction = direction

    def __str__(self):
        return "Room <facing:%s, windows=#%d>" % (self.direction,
                                                  self.nwindows)
class Porch(object):
    """ A class representing a Porch in a house """
    
    def __init__(self, ndoors=2, direction='W'):
        self.ndoors = ndoors
        self.direction = direction

    def __str__(self):
        return "Porch <facing:%s, doors=#%d>" % (self.direction,
                                                 self.ndoors)   
    
class LegoHouse(object):
    """ A lego house class """

    def __init__(self, nrooms=0, nwindows=0,nporches=0):
        # windows per room
        self.nwindows = nwindows
        self.nporches = nporches
        self.nrooms = nrooms
        self.rooms = []
        self.porches = []

    def __str__(self):
        msg="LegoHouse<rooms=#%d, porches=#%d>" % (self.nrooms,
                                                   self.nporches)

        for i in self.rooms:
            msg += str(i)

        for i in self.porches:
            msg += str(i)

        return msg

    def add_room(self,room):
        """ Add a room to the house """
        
        self.rooms.append(room)

    def add_porch(self,porch):
        """ Add a porch to the house """
        
        self.porches.append(porch)

Our example shows three classes, which are as follows:

  • A Room and Porch class each representing a room and porch of a house—a room has windows and doors, and a porch has doors.
  • A LegoHouse class representing a toy example for an actual house (We are imagining a kid building a house with lego blocks here, with rooms and porches.) The Lego house will consist of any number of rooms and porches.

Let's try and create a simple LegoHouse instance with one room and one porch, each with the default configuration:

>>> house = LegoHouse(nrooms=1,nporches=1)
>>> print(house)
LegoHouse<rooms=#1, porches=#1>

Are we done ? No! Notice that our LegoHouse is a class that doesn't fully construct itself in its constructor. The rooms and porches are not really built yet, only their counters are initialized.

So we need to build the rooms and porches separately, and add them to the house. Let's do that:

>>> room = Room(nwindows=1)
>>> house.add_room(room)
>>> porch = Porch()
>>> house.add_porch(porch)
>>> print(house)
LegoHouse<rooms=#1, porches=#1>
Room <facing:S, windows=#1>
Porch <facing:W, doors=#1>

Now you see that our house is fully built. Printing it displays not only the number of rooms and porches, but also details about them. All good!

Now, imagine that you need to build 100 such different house instances, each with different configurations of rooms and porches, and often the rooms themselves have varying numbers of windows and directions!

(Maybe you are building a mobile game which uses Lego Houses where cute little characters such as Trolls or Minions stay and do interesting things.)

It is pretty clear from the example that writing code like the last will not scale to solve the problem.

This is where the Builder pattern can help you. Let's start with a simple LegoHouse builder.

class LegoHouseBuilder(object):
    """ Lego house builder class """

    def __init__(self, *args, **kwargs):
        self.house = LegoHouse(*args, **kwargs)
        
    def build(self):
        """ Build a lego house instance and return it """
        
        self.build_rooms()
        self.build_porches()
        return self.house
    
    def build_rooms(self):
        """ Method to build rooms """
        
        for i in range(self.house.nrooms):
            room = Room(self.house.nwindows)
            self.house.add_room(room)

    def build_porches(self):
        """ Method to build porches """     

        for i in range(self.house.nporches):
            porch = Porch(1)
            self.house.add_porch(porch)

The following are the main aspects of this class:

  • You configure the Builder class with the target class configuration—the number of rooms and porches in this case.
  • It provides a build method, which constructs and assembles (builds) the components of the house—in this case, Rooms and Porches, according to the specified configuration.
  • The build method returns the constructed and assembled house.

Now building different types of Lego Houses with different designs of rooms and porches is just two lines of code:

>>> builder=LegoHouseBuilder(nrooms=2,nporches=1,nwindows=1)
>>> print(builder.build())
LegoHouse<rooms=#2, porches=#1>
Room <facing:S, windows=#1>
Room <facing:S, windows=#1>
Porch <facing:W, doors=#1>

We will now build a similar house, but with rooms that have two windows each:

>>> builder=LegoHouseBuilder(nrooms=2,nporches=1,nwindows=2)
>>> print(builder.build())
LegoHouse<rooms=#2, porches=#1>
Room <facing:S, windows=#2>
Room <facing:S, windows=#2>
Porch <facing:W, doors=#1>

Let's say you find you are continuing to build a lot of Lego Houses with this configuration. You can encapsulate it in a subclass of the Builder so that the preceding code itself is not duplicated a lot:

class SmallLegoHouseBuilder(LegoHouseBuilder):
""" Builder sub-class building small lego house with 1 room and 1porch and rooms having 2 windows """

    def __init__(self):
        self.house = LegoHouse(nrooms=2, nporches=1, nwindows=2)        

Now, the house configuration is burned into the new builder class, and building one is as simple as this:

>>> small_house=SmallLegoHouseBuilder().build()
>>> print(small_house)
LegoHouse<rooms=#2, porches=#1>
Room <facing:S, windows=#2>
Room <facing:S, windows=#2>
Porch <facing:W, doors=#1>

You can also build many of them (say 100, 50 for the Trolls and 50 for the Minions) as follows:

>>> houses=list(map(lambda x: SmallLegoHouseBuilder().build(), range(100)))
>>> print(houses[0])
LegoHouse<rooms=#2, porches=#1>
Room <facing:S, windows=#2>
Room <facing:S, windows=#2>
Porch <facing:W, doors=#1>

>>> len(houses)
100

One can also create more exotic builder classes which do some very specific things. For example, the following is a builder class which creates houses with rooms and porches always facing north:

class NorthFacingHouseBuilder(LegoHouseBuilder):
    """ Builder building all rooms and porches facing North """

    def build_rooms(self):

        for i in range(self.house.nrooms):
            room = Room(self.house.nwindows, direction='N')
            self.house.add_room(room)

    def build_porches(self):

        for i in range(self.house.nporches):
            porch = Porch(1, direction='N')
            self.house.add_porch(porch)


>>> print(NorthFacingHouseBuilder(nrooms=2, nporches=1, nwindows=1).build())
LegoHouse<rooms=#2, porches=#1>
Room <facing:N, windows=#1>
Room <facing:N, windows=#1>
Porch <facing:N, doors=#1>

And, by using Python's multiple inheritance power, one can combine any such builders into new and interesting subclasses. The following, for example, is a builder that produces north-facing small houses:

class NorthFacingSmallHouseBuilder(NorthFacingHouseBuilder, SmallLegoHouseBuilder):
    pass

As expected, it always produces North-facing, small houses with 2 windowed rooms repeatedly. Not very interesting maybe, but very reliable indeed:

>>> print(NorthFacingSmallHouseBuilder().build())
LegoHouse<rooms=#2, porches=#1>
Room <facing:N, windows=#2>
Room <facing:N, windows=#2>
Porch <facing:N, doors=#1>

Before we conclude our discussion on Creational Patterns, let's summarize some interesting aspects of these creational patterns and their interplay, as follows:

  • Builder and Factory: The Builder pattern separates out the assembling process of a class's instance from its creation. A Factory on the other hand is concerned with creating instances of different sub-classes belonging to the same hierarchy using a unified interface. A builder also returns the built instance as a final step, whereas a Factory returns the instance immediately, as there is no separate building step.
  • Builder and Prototype: A Builder can, internally, use a prototype for creating its instances. Further instances from the same builder can then be cloned from this instance. For example, it is instructive to build a Builder class which uses one of our Prototype metaclasses to always clone a prototypical instance.
  • Prototype and Factory: A Prototype factory can, internally, make use of a Factory pattern to build the initial instances of the classes in question.
  • Factory and Singleton: A Factory class is usually a Singleton in traditional programming languages. The other option is to make its methods a class or static method so there is no need to create an instance of the Factory itself. In our examples, we made it a Borg instead.

We will now move on to the next class of patterns: Structural Patterns.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.213.212