Patterns in Python – structural

Structural patterns concern themselves with the intricacies of combining classes or objects to form larger structures that are more than the sum of their parts.

Structural patterns implement this in these two distinct ways:

  • By using class Inheritance to compose classes into one. This is the static approach.
  • By using object composition at runtime to achieve combined functionality. This approach is more dynamic and flexible.

Python, by virtue of supporting multiple inheritance, can implement both of these very well. Being a language with dynamic attributes and using the power of magic methods, Python can also do object composition and the resultant method wrapping pretty well also. So, with Python, a programmer is indeed in a good place with respect to implementing structural patterns.

We will be discussing the following structural patterns in this section: Adapter, Facade, and Proxy.

The Adapter pattern

As the name implies, the Adapter pattern wraps or adapts an existing implementation of a specific interface into another interface which a client expects. The Adapter is also called a Wrapper.

You very often adapt objects into interfaces or types you want when you program, most often without realizing this.

Example:

Look at the following list containing two instances of a fruit and detailing how many:

>>> fruits=[('apples',2), ('grapes',40)]

Let's say you want to quickly find the number of fruits, given a fruit name. The list doesn't allow you to use the fruit as a key, which is a more suitable interface for the operation.

What do you do ? Well, you simply convert the list to a dictionary:

>>> fruits_d=dict(fruits)
>>> fruits_d['apples']
2

Voilà! You got the object in a form that is more convenient for you, adapted to your programming needs. This is a kind of data or object adaptation.

Programmers do such data or object adaptation almost continuously in their code without realizing it. Adaptation of code or data is more common than you think.

Let's consider a class Polygon, representing a regular or irregular Polygon of any shape:

class Polygon(object):
    """ A polygon class """
    
    def __init__(self, *sides):
        """ Initializer - accepts length of sides """
        self.sides = sides
        
    def perimeter(self):
        """ Return perimeter """
        
        return sum(self.sides)
    
    def is_valid(self):
        """ Is this a valid polygon """
        
        # Do some complex stuff - not implemented in base class
        raise NotImplementedError
    
    def is_regular(self):
        """ Is a regular polygon ? """
        
        # True: if all sides are equal
        side = self.sides[0]
        return all([x==side for x in self.sides[1:]])
    
    def area(self):
        """ Calculate and return area """
        
        # Not implemented in base class
        raise NotImplementedError

This preceding class describes a generic, closed Polygon geometric figure in geometry.

Note

We have implemented some basic methods such as perimeter and is_regular, the latter returning whether the Polygon is a regular one such as a hexagon or pentagon.

Let's say we want to implement specific classes for a few regular geometric shapes such as a triangle or rectangle. We can implement these from scratch, of course. However, since a Polygon class is available, we can try to reuse it, and adapt it to our needs.

Let's say the Triangle class requires the following methods:

  • is_equilateral: Returns whether the triangle is an equilateral one
  • is_isosceles: Returns whether the triangle is an isosceles triangle
  • is_valid: Implements the is_valid method for a triangle
  • area: Implements the area method for a triangle

Similarly the Rectangle class, needs the following methods:

  • is_square: Returns whether the rectangle is a square
  • is_valid: Implements the is_valid method for a rectangle
  • area: Implements the area method for a rectangle

The following is the code for an adapter pattern, reusing the Polygon class for the Triangle and Rectangle classes.

The following is the code for the Triangle class:

import itertools 

class InvalidPolygonError(Exception):
    pass

class Triangle(Polygon):
    """ Triangle class from Polygon using class adapter """
    
    def is_equilateral(self):
        """ Is this an equilateral triangle ? """
        
        if self.is_valid():
            return super(Triangle, self).is_regular()
    
    def is_isosceles(self):
        """ Is the triangle isosceles """
        
        if self.is_valid():
            # Check if any 2 sides are equal
            for a,b in itertools.combinations(self.sides, 2):
                if a == b:
                    return True
        return False
    
    def area(self):
        """ Calculate area """
        
        # Using Heron's formula
        p = self.perimeter()/2.0
        total = p
        for side in self.sides:
            total *= abs(p-side)
            
        return pow(total, 0.5)
    
    def is_valid(self):
        """ Is the triangle valid """
        
        # Sum of 2 sides should be > 3rd side
        perimeter = self.perimeter()
        for side in self.sides:
            sum_two = perimeter - side
            if sum_two <= side:
                raise InvalidPolygonError(str(self.__class__) + "is invalid!")
                
        return True

Take a look at the following Rectangle class:

class Rectangle(Polygon):
    """ Rectangle class from Polygon using class adapter """

    def is_square(self):
        """ Return if I am a square """

        if self.is_valid():
            # Defaults to is_regular
            return self.is_regular()

    def is_valid(self):
        """ Is the rectangle valid """

        # Should have 4 sides
        if len(self.sides) != 4:
            return False

        # Opposite sides should be same
        for a,b in [(0,2),(1,3)]:
            if self.sides[a] != self.sides[b]:
                return False

        return True

    def area(self):
        """ Return area of rectangle """

        # Length x breadth
        if self.is_valid():
            return self.sides[0]*self.sides[1]

Now let's see classes in action.

Let's create an equilateral triangle for the first test:

>>> t1 = Triangle(20,20,20)
>>> t1.is_valid()
True

An equilateral triangle is also isosceles:

>>> t1.is_equilateral()
True
>>> t1.is_isosceles()
True

Let's calculate the area:

>>> t1.area()
173.20508075688772

Let's try a triangle which is not valid:

>>> t2 = Triangle(10, 20, 30)
>>> t2.is_valid()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/anand/Documents/ArchitectureBook/code/chap7/adapter.py", line 75, in is_valid
    raise InvalidPolygonError(str(self.__class__) + "is invalid!")
adapter.InvalidPolygonError: <class 'adapter.Triangle'>is invalid!

Note

Its dimensions show it is a straight line, not a triangle. The is_valid method is not implemented in the base class, hence the subclasses need to override it to provide a proper implementation. In this case, we raise an exception if the triangle is invalid.

The following is an illustration of the Rectangle class in action:

>>> r1 = Rectangle(10,20,10,20)
>>> r1.is_valid()
True
>>> r1.area()
200
>>> r1.is_square()
False
>>> r1.perimeter()
60

Let's create a square:

>>> r2 = Rectangle(10,10,10,10)
>>> r2.is_square()
True

The Rectangle/Triangle classes shown here are examples of class adapters. This is because they inherit the class that they want to adapt, and provide the methods expected by the client, often delegating the computation to the base-class's methods. This is evident in the is_equilateral and is_square methods of the Triangle and Rectangle classes respectively.

Let's look at an alternative implementation of the same classes—this time, via object composition, in other words, object adapters:

import itertools

class Triangle (object) :
    """ Triangle class from Polygon using class adapter """

    def __init__(self, *sides):
        # Compose a polygon
        self.polygon = Polygon(*sides)

    def perimeter(self):
        return self.polygon.perimeter()
    
    def is_valid(f):
        """ Is the triangle valid """

        def inner(self, *args):
            # Sum of 2 sides should be > 3rd side
            perimeter = self.polygon.perimeter()
            sides = self.polygon.sides
            
            for side in sides:
                sum_two = perimeter - side
                if sum_two <= side:
                    raise InvalidPolygonError(str(self.__class__) + "is invalid!")

            result = f(self, *args)
            return result
        
        return inner

    @is_valid
    def is_equilateral(self):
        """ Is this equilateral triangle ? """
        
        return self.polygon.is_regular()

    @is_valid
    def is_isosceles(self):
        """ Is the triangle isoscles """
        
        # Check if any 2 sides are equal
        for a,b in itertools.combinations(self.polygon.sides, 2):
            if a == b:
                return True
        return False
    
    def area(self):
        """ Calculate area """
        
        # Using Heron's formula
        p = self.polygon.perimeter()/2.0
        total = p
        for side in self.polygon.sides:
            total *= abs(p-side)
            
        return pow(total, 0.5)

This class works similarly to the other one, even though the internal details are implemented via object composition rather than class inheritance:

>>> t1=Triangle(2,2,2)
>>> t1.is_equilateral()
True
>>> t2 = Triangle(4,4,5)
>>> t2.is_equilateral()
False
>>> t2.is_isosceles()
True

The main differences between this implementation and the class adapter are as follows:

  • The object adapter class doesn't inherit from the class we want to adapt from. Instead, it composes an instance of the class.
  • Any wrapper methods are forwarded to the composed instance, for example, the perimeter method.
  • All attribute access to the wrapped instance has to be specified explicitly in this implementation. Nothing comes for free since we are not inheriting the class. (For example, inspect the way we access the sides attribute of the enclosed polygon instance.)

Note

Observe how we converted the previous is_valid method to a decorator in this implementation. This is because many methods carry out a first check on is_valid, and then perform their actions, so it is an ideal candidate for a decorator. This also aids rewriting this implementation to a more convenient form, which is discussed next.

One problem with the object adapter implementation, as shown in the preceding implementation, is that any attribute reference to the enclosed adapted instance has to be made explicitly. For example, had we forgotten to implement the perimeter method for the Triangle class here, there would have been no method at all to call, as we aren't inheriting from the Adapter class.

The following is an alternate implementation, which makes use of the power of one of Python's magic methods, namely __getattr__, to simplify this. We are demonstrating this implementation on the Rectangle class:

class Rectangle(object):
    """ Rectangle class from Polygon using object adapter """


    method_mapper = {'is_square': 'is_regular'}
    
    def __init__(self, *sides):
        # Compose a polygon
        self.polygon = Polygon(*sides)

    def is_valid(f):
        def inner(self, *args):
            """ Is the rectangle valid """

            sides = self.sides
            # Should have 4 sides
            if len(sides) != 4:
                return False

            # Opposite sides should be same
            for a,b in [(0,2),(1,3)]:
                if sides[a] != sides[b]:
                    return False

            result = f(self, *args)
            return result
        
        return inner

    def __getattr__(self, name):
        """ Overloaded __getattr__ to forward methods to wrapped instance """

        if name in self.method_mapper:
            # Wrapped name
            w_name = self.method_mapper[name]
            print('Forwarding to method',w_name)
            # Map the method to correct one on the instance
            return getattr(self.polygon, w_name)
        else:
            # Assume method is the same
            return getattr(self.polygon, name)
        
    @is_valid
    def area(self):
        """ Return area of rectangle """

        # Length x breadth
        sides = self.sides      
        return sides[0]*sides[1]

Let's look at examples using this class:

>>> r1=Rectangle(10,20,10,20)
>>> r1.perimeter()
60
>>> r1.is_square()
Forwarding to method is_regular
False

You can see that we are able to call the method is_perimeter on the Rectangle instance even though no such method is actually defined on the class. Similarly, is_square seems to work magically. What is happening here?

The magic method __getattr__ is invoked by Python on an object if it cannot find an attribute in the usual ways – by first looking up the object's dictionary, then its class's dictionary, and so on. It takes a name, and hence provides a hook on a class, to implement a way to provide method lookups by routing them to other objects.

In this case, the __getattr__ method does the following:

  • Checks for the attribute name in the method_mapper dictionary. This is a dictionary we have created on the class, which maps a method name that we want to call on the class (as a key) to the actual method name on the wrapped instance (as a value). If an entry is found, it is returned.
  • If no entry is found on the method_mapper dictionary, the entry is passed as such to the wrapped instance to be looked up by the same name.
  • We use getattr in both cases to look up and return the attribute from the wrapped instance.
  • Attributes can be anything—data attributes or methods. For example, see how we refer to the sides attribute of the wrapped polygon instance as if it belonged to the Rectangle class in the method area and the is_valid decorator.
  • If an attribute is not present on the wrapped instance, it raises an AttributeError:
        >>> r1.convert_to_parallelogram(angle=30)
        Traceback (most recent call last):
          File "<stdin>", line 1, in <module>
         File "adapter_o.py", line 133, in __getattr__
            return getattr(self.polygon, name)
        AttributeError: 'Polygon' object has no attribute 'convert_to_parallelogram'

Object adapters implemented using this technique are much more versatile, and lead to less code than regular object adapters where every method has to be explicitly written and forwarded to the wrapped instance.

The Facade pattern

A facade is a structural pattern that provides a unified interface to multiple interfaces in a subsystem. The Facade pattern is useful where a system consists of multiple subsystems, each with its own interfaces, but presents some high-level functionality, which needs to be captured, as a general top-level interface to the client.

A classic example of an object in everyday life which is a Facade is an automobile.

For example, a car consists of an engine, power train, axle and wheel assembly, electronics, steering systems, brake systems, and other such components.

However, usually, you don't have to bother whether the brake in your car is a disc-brake, or whether its suspension is coil-spring or McPherson struts, do you?

This is because the car manufacturer has provided a Facade for you to operate and maintain the car which reduces the complexity and provides you with simpler sub-systems which are easy to operate by themselves, such as the following:

  • The ignition system to start the car
  • The steering system to maneuver it
  • The clutch-accelerator-brake system to control it
  • The gear and transmission system to manage the power and speed

A lot of complex systems around us are Facades. Like the car example, a computer is a Facade, an Industrial Robot is another. All factory control systems are facades, supplying a few dashboards and controls for the engineer to tweak the complex systems behind it, and keep them running.

Facades in Python

The Python standard library contains a lot of modules which are good examples of Facades. The compiler module, which provides hooks to parse and compile Python source code, is a Facade to the lexer, parser, AST tree generator, and the like.

The following shows the help contents of this module:

Facades in Python

In the next page of the help contents, you can see how this module acts as a facade to other modules which are used to implement the functions defined in this package. (Look at PACKAGE CONTENTS at the bottom of the screenshot):

Facades in Python

Let's look at sample code for a Facade pattern. In this example, we will model a Car with a few of its multiple subsystems.

The following is the code for all the subsystems:

class Engine(object):
    """ An Engine class """
    
    def __init__(self, name, bhp, rpm, volume, cylinders=4, type='petrol'):
        self.name = name
        self.bhp = bhp
        self.rpm = rpm
        self.volume = volume
        self.cylinders = cylinders
        self.type = type

    def start(self):
        """ Fire the engine """
        print('Engine started')

    def stop(self):
        """ Stop the engine """
        print('Engine stopped')

class Transmission(object):
    """ Transmission class """

    def __init__(self, gears, torque):
        self.gears = gears
        self.torque = torque
        # Start with neutral
        self.gear_pos = 0

    def shift_up(self):
        """ Shift up gears """

        if self.gear_pos == self.gears:
            print('Cannot shift up anymore')
        else:
            self.gear_pos += 1
            print('Shifted up to gear',self.gear_pos)

    def shift_down(self):
        """ Shift down gears """

        if self.gear_pos == -1:
            print("In reverse, can't shift down")
        else:
            self.gear_pos -= 1
            print('Shifted down to gear',self.gear_pos)         

    def shift_reverse(self):
        """ Shift in reverse """

        print('Reverse shifting')
        self.gear_pos = -1

    def shift_to(self, gear):
        """ Shift to a gear position """

        self.gear_pos = gear
        print('Shifted to gear',self.gear_pos)      

                 
class Brake(object):
    """ A brake class """

    def __init__(self, number, type='disc'):
        self.type = type
        self.number = number

    def engage(self):
        """ Engage the break """

        print('%s %d engaged' % (self.__class__.__name__,
                                 self.number))

    def release(self):
        """ Release the break """

        print('%s %d released' % (self.__class__.__name__,
                                  self.number))

class ParkingBrake(Brake):
    """ A parking brake class """

    def __init__(self, type='drum'):
        super(ParkingBrake, self).__init__(type=type, number=1)
        

class Suspension(object):
    """ A suspension class """
    
    def __init__(self, load, type='mcpherson'):
        self.type = type
        self.load = load

class Wheel(object):
    """ A wheel class """

    def __init__(self, material, diameter, pitch):
        self.material = material
        self.diameter = diameter
        self.pitch = pitch
                 
class WheelAssembly(object):
    """ A wheel assembly class """
    
    def __init__(self, brake, suspension):
        self.brake = brake
        self.suspension = suspension
        self.wheels = Wheel('alloy', 'M12',1.25)

    def apply_brakes(self):
        """ Apply brakes """

        print('Applying brakes')
        self.brake.engage()

class Frame(object):
    """ A frame class for an automobile """
    
    def __init__(self, length, width):
        self.length = length
        self.width = width

As you can see, we have covered a good number of the subsystems in a car, or those which are essential, at least.

The following code for the Car class combines them as a Facade with two methods, to start and stop the car:

class Car(object):
    """ A car class - Facade pattern """

    def __init__(self, model, manufacturer):
        self.engine = Engine('K-series',85,5000, 1.3)
        self.frame = Frame(385, 170)
        self.wheel_assemblies = []
        for i in range(4):
            self.wheel_assemblies.append(WheelAssembly(Brake(i+1), Suspension(1000)))
            
        self.transmission = Transmission(5, 115)
        self.model = model
        self.manufacturer = manufacturer
        self.park_brake = ParkingBrake()
        # Ignition engaged
        self.ignition = False

    def start(self):
        """ Start the car """

        print('Starting the car')
        self.ignition = True
        self.park_brake.release()
        self.engine.start()
        self.transmission.shift_up()
        print('Car started.')

    def stop(self):
        """ Stop the car """

        print('Stopping the car')
        # Apply brakes to reduce speed
        for wheel_a in self.wheel_assemblies:
            wheel_a.apply_brakes()

        # Move to 2nd gear and then 1st
        self.transmission.shift_to(2)
        self.transmission.shift_to(1)
        self.engine.stop()
        # Shift to neutral
        self.transmission.shift_to(0)
        # Engage parking brake
        self.park_brake.engage()
        print('Car stopped.')

Let's build an instance of the Car first:

>>> car = Car('Swift','Suzuki')
>>> car
<facade.Car object at 0x7f0c9e29afd0>

Let's now take the car out of the garage and go for a spin:

>>> car.start()
Starting the car
ParkingBrake 1 released
Engine started
Shifted up to gear 1

From the preceding output you can see that our car has started.

Now that we have driven it for a while, we can stop the car. As you may have guessed, stopping is more involved than starting!

>>> car.stop()
Stopping the car
Shifted to gear 2
Shifted to gear 1
Applying brakes
Brake 1 engaged
Applying brakes
Brake 2 engaged
Applying brakes
Brake 3 engaged
Applying brakes
Brake 4 engaged
Engine stopped
Shifted to gear 0
ParkingBrake 1 engaged
Car stopped.
>>>

Facades are useful for taking the complexity out of systems so that working with them becomes easier. As the preceding example shows, it would've been awfully difficult if we hadn't built the start and stop methods the way we did in this example. These methods hide the complexity behind the actions involved with subsystems in starting and stopping a Car.

This is what a Facade does best.

The proxy pattern

A proxy pattern wraps another object to control access to it. Some usage scenarios are as follows:

  • We need a virtual resource closer to the client, which acts in place of the real resource in another network, for example, a remote proxy.
  • We need to control/monitor access to a resource, for example, a network proxy and an instance counting proxy.
  • We need to protect a resource or object (protection proxy) because direct access to it would cause security issues or compromise it, for example, a reverse proxy server.
  • We need to optimize access to results from a costly computation or network operation so that the computation is not performed every time, for example, a caching proxy

A proxy always implements the interface of the object it is proxying to, its target in other words. This can be either via inheritance or via composition. In Python, the latter can be done more powerfully by overriding the __getattr__ method, as we've seen in the Adapter example.

An instance-counting proxy

We will start with an example that demonstrates using the proxy pattern to keep track of instances of a class. We will reuse our Employee class and its subclasses from the Factory pattern here:

class EmployeeProxy(object):
    """ Counting proxy class for Employees """

    # Count of employees
    count = 0

    def __new__(cls, *args):
        """ Overloaded __new__ """
        # To keep track of counts
        instance = object.__new__(cls)
        cls.incr_count()
        return instance
        
    def __init__(self, employee):
        self.employee = employee

    @classmethod
    def incr_count(cls):
        """ Increment employee count """
        cls.count += 1

    @classmethod
    def decr_count(cls):
        """ Decrement employee count """
        cls.count -= 1

    @classmethod
    def get_count(cls):
        """ Get employee count """
        return cls.count
    
    def __str__(self):
        return str(self.employee)
    
    def __getattr__(self, name):
        """ Redirect attributes to employee instance """

        return getattr(self.employee, name)
        
    def __del__(self):
        """ Overloaded __del__ method """
        # Decrement employee count
        self.decr_count()

class EmployeeProxyFactory(object):
    """ An Employee factory class returning proxy objects """

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

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

Note

We haven't duplicated the code for the employee subclasses, as these are already available in the Factory pattern discussion.

We have two classes here: the EmployeeProxy and the original factory class modified to return instances of EmployeeProxy instead of employee. The modified factory class makes it easy for us to create proxy instances instead of having to do it ourselves.

The proxy, as implemented here, is a composition or object proxy, as it wraps around the target object (employee) and overloads __getattr__ to redirect attribute access to it. It keeps track of the count of instances by overriding the __new__ and __del__ methods for instance creation and instance deletion respectively.

Let's see an example of using the Proxy:

>>> factory = EmployeeProxyFactory()
>>> engineer = factory.create('engineer','Sam',25,'M')
>>> print(engineer)
Engineer - Sam, 25 years old M

Note

This prints details of the engineer via proxy, since we have overridden the __str__ method in the proxy class, which calls the same method of the employee instance.

>>> admin = factory.create('admin','Tracy',32,'F')
>>> print(admin)
Admin - Tracy, 32 years old F

Let's check the instance count now. This can be done either via the instances or via the class, since anyway it references a class variable:

>>> admin.get_count()
2
>>> EmployeeProxy.get_count()
2

Let's delete the instances, and see what happens!

>>> del engineer
>>> EmployeeProxy.get_count()
1
>>> del admin
>>> EmployeeProxy.get_count()
0

Note

The weak reference module in Python provides a proxy object which performs something very similar to what we have implemented, by proxying access to class instances.

The following is an example:

>>> import weakref
>>> import gc
>>> engineer=Engineer('Sam',25,'M')

Let's check the reference count of the new object:

>>> len(gc.get_referrers(engineer))
1

Now create a weak reference to it:

>>> engineer_proxy=weakref.proxy(engineer)

The weakref object acts in all respects like the object it's proxying for:

>>> print(engineer_proxy)
Engineer - Sam, 25 years old M
>>> engineer_proxy.get_role()
'engineering'

However, note that a weakref proxy doesn't increase the reference count of the proxied object:

>>> len(gc.get_referrers(engineer))
      1
..................Content has been hidden....................

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