Follow Techotopia on Twitter

On-line Guides
All Guides
eBook Store
iOS / Android
Linux for Beginners
Office Productivity
Linux Installation
Linux Security
Linux Utilities
Linux Virtualization
Linux Kernel
System/Network Admin
Programming
Scripting Languages
Development Tools
Web Development
GUI Toolkits/Desktop
Databases
Mail Systems
openSolaris
Eclipse Documentation
Techotopia.com
Virtuatopia.com
Answertopia.com

How To Guides
Virtualization
General System Admin
Linux Security
Linux Filesystems
Web Servers
Graphics & Desktop
PC Hardware
Windows
Problem Solutions
Privacy Policy

  




 

 

Chapter 23. Some Design Patterns

The best way to learn object-oriented design is to look at patterns for common solutions to ubiquitous problems. These patterns are often described with a synopsis that gives you several essential features. The writer of a pattern will describe a programming context, the specific problem, the forces that lead to various kinds of solutions, a solution that optimizes the competing forces, and any consequences of choosing this solution.

There are a number of outstanding books on patterns. We'll pick a few key patterns from one of the books, and develop representative classes in some depth. The idea is to add a few additional Python programming techniques, along with a number of class design techniques.

Most of these patterns come from the “Gang of Four” design patterns book, Design Patterns: Elements of Reusable Object-Oriented Software [Gamma99]. We'll look at a few design patterns that illustrate some useful Python programming techniques.

Factory Method

This is a pattern for designing a class which is used as a factory for a family of other classes. This allows a client program to use a very flexible and extensible Factory to create necessary objects.

State

This is a pattern for desiging a hierarchy of classes that describes states (or status) and state-specific processing or data.

Strategy

This is a pattern that helps create a class that supports an extension in the form of alternative strategies for implementing methods.

Factory Method

When we add subclasses to a class hierarchy, we may also need to rearrange the statements where objects are created. To provide a flexible implementation, it is generally a good idea to centralize all of the object creation statements into a single method of class. When we extend the subclass hierarchy we can also create a relevant subclass of the centralized object creation class.

The design pattern for this kind of centralized object creator can be called a Factory. It contains the details for creating an instance of each of the various subclasses.

In the next section, Part IV, “Components, Modules and Packages”, we'll look at how to package a class hierarchy in a module. Often the classes and the factory object are bundled into one seamless module. Further, as the module evolves and improves, we need to preserve the factory which creates instances of other classes in the module. Creating a class with a factory method helps us control the evolution of a module. If we omit the Factory Method , then everyone who uses our module has to rewrite their programs when we change our class hierarchy.

Extending the Card Class Hierarchy. We'll extend the Card class hierarchy, introduced in the section called “Inheritance”. That original design had three classes: Card, FaceCard and AceCard.

While this seems complete for basic Blackjack, we may need to extend these classes. For example, if we are going to simulate a common card counting technique, we'll need to separate 2-6 from 7-9, leading to two more subclasses. Adding subclasses can easily ripple through an application, leading to numerous additional, sometimes complex changes. We would have to look for each place where the various subclasses of cards were created. The Factory design pattern, however, provides a handy solution to this problem.

An object of a class based on the Factory pattern creates instances of other classes. This saves having to place creation decisions throughout a complex program. Instead, all of the creation decision-making is centralized in the factory class.

For our card example, we can define a CardFactory that creates new instances of Card (or the appropriate subclass.)

class CardFactory( object ):
    def newCard( self, rank, suit ):
        if rank == 1:
            return Ace( rank, suit )
        elif rank in [ 11, 12, 13 ]:
            return FaceCard( rank, suit )
        else:
            return Card( rank, suit )

We can simplify our version of Deck using this factory.

class Deck( object ):
    def __init__( self ):
        factory= CardFactory()
        self.cards = [ factory.newCard( rank+1, suit )
            for suit in range(4)
                for rank in range(13) ]
    
Rest of the class is the same

Centralized Object Creation

While it may seem like overhead to centralize object creation in factory objects, it has a number of benefits.

First, and foremost, centralizing object creation makes it easy to locate the one place where objects are constructed, and fix the constructor. Having object construction scattered around an application means that time is spent searching for and fixing things that are, in a way, redundant.

Additionally, centralized object creation is the norm for larger applications. When we break down an application into the data model, the view objects and the control objects, we find at least two kinds of factories. The data model elements are often created by fetching from a database, or parsing an input file. The control objects are part of our application that are created during initialization, based on configuration parameters, or created as the program runs based on user inputs.

Finally, it makes evolution of the application possible when we are creating a new version of a factory rather than tracking down numerous creators scattered around an application. We can assure ourselves that the old factory is still available and still passes all the unit tests. The new factory creates the new objects for the new features of the application software.

Exetending the Factory. By using this kind of Factory method design pattern, we can more easily create new subclasses of Card. When we create new subclasses, we do three things:

  1. Extend the Card class hierarchy to define the additional subclasses.

  2. Extend the CardFactory creation rules to create instances of the new subclasses. This is usually done by creating a new subclass of the factory.

  3. Extend or update Deck to use the new factory. We can either create a new subclass of Deck, or make the factory object a parameter to Deck.

Let's create some new subclasses of Card for card counting. These will subdivide the number cards into low, neutral and high ranges. We'll also need to subclass our existing FaceCard and Ace classes to add this new method.

class CardHi( Card ):
    """Used for 10."""
    def count( self ): return -1

class CardLo( Card ):
    """Used for 3, 4, 5, 6, 7."""
    def count( self ): return +1

class CardNeutral( Card ):
    """Used for 2, 8, 9."""
    def count( self ): return 0

class FaceCount( FaceCard ):
    """Used for J, Q and K"""
    def count( self ): return -1

class AceCount( Ace ):
    """Used for A"""
    def count( self ): return -1

A counting subclass of Hand can sum the count values of all Card instances to get the count of the deck so far.

Once we have our new subclasses, we can create a subclass of CardFactory to include these new subclasses of Card. We'll call this new class HiLoCountFactory. This new subclass will define a new version of the newCard method that creates appropriate objects.

By using default values for parameters, we can make this factory option transparent. We can design Deck to use the original CardFactory by default. We can also design Deck to accept an optional CardFactory object, which would tailor the Deck for a particular player strategy.

class Deck( object ):
    def __init__( self, factory=CardFactory() ):
        self.cards = [ factory.newCard( rank+1, suit )
            for suit in range(4)
                for rank in range(13) ]
    
Rest of the class is the same

The Overall Main Program. Now we can have main programs that look something like the following.

d1 = Deck()
d2 = Deck(HiLoCountFactory())

In this case, d1 is a Deck using the original definitions, ignoring the subclasses for card counting. The d2 Deck is built using a different factory and has cards that include a particular card counting strategy.

We can now introduce variant card-counting schemes by introducing further subclasses of Card and CardFactory. To pick a particular set of card definitions, the application creates an instance of one of the available subclasses of CardFactory. Since all subclasses have the same newCard method, the various objects are interchangeable. Any CardFactory object can be used by Deck to produce a valid deck of cards.

This evolution of a design via new subclasses is a very important technique of object-oriented programming. If we add features via subclasses, we are sure that the original definitions have not been disturbed. We can be completely confident that adding a new feature to a program will not break old features.


 
 
  Published under the terms of the Open Publication License Design by Interspire