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 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:
__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
def test_single(cls): """ Test if passed class is a singleton """ return cls() == cls()
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:
__init__
method. This is what we are doing here for the _instance
class variable, which will hold the single instance of the class.__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.Let's take a look at the pros and cons in the metaclass approach over the class approach:
Now let's think out of the box, and see if we can solve the Singleton problem in a slightly different way.
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:
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 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:
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.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 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:
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'>
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.
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
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:
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.
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
It is instructive to discuss a few aspects of the specific example we have chosen if the reader hasn't observed them already:
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.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.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.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.
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:
Room
and Porch
class each representing a room and porch of a house—a room has windows and doors, and a porch has doors.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:
build
method, which constructs and assembles (builds) the components of the house—in this case, Rooms
and Porches
, according to the specified configuration.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:
We will now move on to the next class of patterns: Structural Patterns.
3.135.213.212