Recipe6.14.Implementing the State Design Pattern


Recipe 6.14. Implementing the State Design Pattern

Credit: Elmar Bschorer

Problem

An object in your program can switch among several "states", and the object's behavior must change along with the object's state.

Solution

The key idea of the State Design Pattern is to objectify the "state" (with its several behaviors) into a class instance (with its several methods). In Python, you don't have to build an abstract class to represent the interface that is common to the various states: just write the classes for the "state"s themselves. For example:

class TraceNormal(object):     ' state for normal level of verbosity '     def startMessage(self):         self.nstr = self.characters = 0     def emitString(self, s):         self.nstr += 1         self.characters += len(s)     def endMessage(self):         print '%d characters in %d strings' % (self.characters, self.nstr) class TraceChatty(object):     ' state for high level of verbosity '     def startMessage(self):         self.msg = [  ]     def emitString(self, s):         self.msg.append(repr(s))     def endMessage(self):         print 'Message: ', ', '.join(self.msg) class TraceQuiet(object):     ' state for zero level of verbosity '     def startMessage(self): pass     def emitString(self, s): pass     def endMessage(self): pass class Tracer(object):     def _ _init_ _(self, state): self.state = state     def setState(self, state): self.state = state     def emitStrings(self, strings):         self.state.startMessage( )         for s in strings: self.state.emitString(s)         self.state.endMessage( ) if _ _name_ _ == '_ _main_ _':     t = Tracer(TraceNormal( ))     t.emitStrings('some example strings here'.split( )) # emits: 21 characters in 4 strings     t.setState(TraceQuiet( ))     t.emitStrings('some example strings here'.split( )) # emits nothing     t.setState(TraceChatty( ))     t.emitStrings('some example strings here'.split( )) # emits: Message: 'some', 'example', 'strings', 'here'

Discussion

With the State Design Pattern, you can "factor out" a number of related behaviors of an object (and possibly some data connected with these behaviors) into an auxiliary state object, to which the main object delegates these behaviors as needed, through calls to methods of the "state" object. In Python terms, this design pattern is related to the idioms of rebinding an object's whole _ _class_ _, as shown in Recipe 6.11, and rebinding just certain methods (shown in Recipe 2.14). This design pattern, in a sense, lies in between those Python idioms: you group a set of related behaviors, rather than switching either all behavior, by changing the object's whole _ _class_ _, or each method on its own, without grouping. With relation to the classic design pattern terminology, this recipe presents a pattern that falls somewhere between the classic State Design Pattern and the classic Strategy Design Pattern.

This State Design Pattern has some extra oomph, compared to the related Pythonic idioms, because an appropriate amount of data can live together with the behaviors you're delegatingexactly as much, or as little, as needed to support each specific behavior. In the examples given in this recipe's Solution, for example, the different state objects differ greatly in the kind and amount of data they need: none at all for class TraceQuiet, just a couple of numbers for TraceNormal, a whole list of strings for TraceChatty. These responsibilities are usefully delegated from the main object to each specific "state object".

In some cases, although not in the specific examples shown in this recipe, state objects may need to cooperate more closely with the main object, by calling main object methods or accessing main object attributes in certain circumstances. To allow this, the main object can pass as an argument either self or some bound method of self to methods of the "state" objects. For example, suppose that the functionality in this recipe's Solution needs to be extended, in that the main object must keep track of how many lines have been emitted by messages it has sent. Tracer._ _init_ _ will have to add one per-instance initialization self.lines = 0, and the signature of the "state" object's endMessage methods will have to be extended to def endMessage(self, tracer):. The implementation of endMessage in class TraceQuiet will just ignore the tracer argument, since it doesn't actually emit any lines; the implementations in the other two classes will each add a statement tracer.lines += 1, since each of them emits one line per message.

As you see, the kind of closer coupling implied by this kind of extra functionality need not be particularly problematic. In particular, the key feature of the classic State Design Pattern, that state objects are the ones that handle state switching (while, in the Strategy Design Pattern, the switching comes from the outside), is just not enough of a big deal in Python to warrant considering the two design patterns as separate.

See Also

See http://exciton.cs.rice.edu/JavaResources/DesignPatterns/ for good coverage of the classic design patterns, albeit in a Java context.



Python Cookbook
Python Cookbook
ISBN: 0596007973
EAN: 2147483647
Year: 2004
Pages: 420

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net