DekGenius.com
[ Team LiB ] Previous Section Next Section

22.4 OOP and Composition: "has-a" Relationships

We introduced the notion of composition in Chapter 19. From a programmer's point of view, composition involves embedding other objects in a container object and activating them to implement container methods. To a designer, composition is another way to represent relationships in a problem domain. But rather than set membership, composition has to do with components—parts of a whole. Composition also reflects the relationships between parts; it's usually called a "has-a" relationship. Some OO design texts refer to composition as aggregation (or distinguish between the two terms by using aggregation for a weaker dependency between container and contained); in this text, "composition" simply refers to a collection of embedded objects.

Now that we've implemented our employees, let's put them in the pizza shop and let them get busy. Our pizza shop is a composite object; it has an oven, and employees like servers and chefs. When a customer enters and places an order, the components of the shop spring into action—the server takes an order, the chef makes the pizza, and so on. The following example, file pizzashop.py, simulates all the objects and relationships in this scenario:

from employees import PizzaRobot, Server

class Customer:
    def __init__(self, name):
        self.name = name
    def order(self, server):
        print self.name, "orders from", server
    def pay(self, server):
        print self.name, "pays for item to", server

class Oven:
    def bake(self):
        print "oven bakes"

class PizzaShop:
    def __init__(self):
        self.server = Server('Pat')         # Embed other objects.
        self.chef   = PizzaRobot('Bob')     # A robot named bob
        self.oven   = Oven(  )

    def order(self, name):
        customer = Customer(name)           # Activate other objects.
        customer.order(self.server)         # Customer orders from server.
        self.chef.work(  )
        self.oven.bake(  )
        customer.pay(self.server)

if __name__ == "__main__":
    scene = PizzaShop(  )                        # Make the composite.
    scene.order('Homer')                    # Simulate Homer's order.
    print '...'
    scene.order('Shaggy')                   # Simulate Shaggy's order.

The PizzaShop class is a container and controller; its constructor makes and embeds instances of the employee classes we wrote in the last section, as well as an Oven class defined here. When this module's self-test code calls the PizzaShop order method, the embedded objects are asked to carry out their actions in turn. Notice that we make a new Customer object for each order, and pass on the embedded Server object to Customer methods; customers come and go, but the server is part of the pizza shop composite. Also notice that employees are still involved in an inheritance relationship; composition and inheritance are complementary tools:

C:\python\examples> python pizzashop.py
Homer orders from <Employee: name=Pat, salary=40000>
Bob makes pizza
oven bakes
Homer pays for item to <Employee: name=Pat, salary=40000>
...
Shaggy orders from <Employee: name=Pat, salary=40000>
Bob makes pizza
oven bakes
Shaggy pays for item to <Employee: name=Pat, salary=40000>

When we run this module, our pizza shop handles two orders—one from Homer, and then one from Shaggy. Again, this is mostly just a toy simulation, but the objects and interactions are representative of composites at work. As a rule of thumb, classes can represent just about any objects and relationships you can express in a sentence; just replace nouns with classes and verbs with methods, and you have a first cut at a design.

22.4.1 Stream Processors Revisited

For a more realistic composition example, recall the generic data stream processor function partially coded in the introduction to OOP in Chapter 19:

def processor(reader, converter, writer):
    while 1:
        data = reader.read(  )
        if not data: break
        data = converter(data)
        writer.write(data)

Rather than using a simple function here, we might code this as a class that uses composition to do its work, to provide more structure, and support inheritance. File streams.py demonstrates one way to code this:

class Processor:
    def __init__(self, reader, writer):
        self.reader = reader
        self.writer = writer
    def process(self):
        while 1:
            data = self.reader.readline(  )
            if not data: break
            data = self.converter(data)
            self.writer.write(data)
    def converter(self, data):
        assert 0, 'converter must be defined'

Coded this way, reader and writer objects are embedded within the class instance (composition), and we supply the converter logic in a subclass rather than passing in a converter function (inheritance). File converters.py shows how:

from streams import Processor

class Uppercase(Processor):
    def converter(self, data):
        return data.upper(  )

if __name__ == '__main__':
    import sys
    Uppercase(open('spam.txt'), sys.stdout).process(  )

Here, the Uppercase class inherits the stream processing loop logic (and anything else that may be coded in its superclasses). It needs to define only the thing that is unique about it—the data conversion logic. When this file is run, it makes and runs an instance, which reads from file spam.txt and writes the uppercase equivalent of that file to the stdout stream:

C:\lp2e> type spam.txt
spam
Spam
SPAM!

C:\lp2e> python converters.py
SPAM
SPAM
SPAM!

To process different sorts of streams, pass in different sorts of objects to the class construction call. Here, we use an output file instead of a stream:

C:\lp2e> python
>>> import converters
>>> prog = converters.Uppercase(open('spam.txt'), open('spamup.txt', 'w'))
>>> prog.process(  )

C:\lp2e> type spamup.txt
SPAM
SPAM
SPAM!

But as suggested earlier, we could also pass in arbitrary objects wrapped up in classes that define the required input and output method interfaces. Here's a simple example that passes in a writer class that wraps up the text inside HTML tags:

C:\lp2e> python
>>> from converters import Uppercase
>>>
>>> class HTMLize:
...     def write(self, line): 
...         print '<PRE>%s</PRE>' % line[:-1]
...
>>> Uppercase(open('spam.txt'), HTMLize(  )).process(  )
<PRE>SPAM</PRE>
<PRE>SPAM</PRE>
<PRE>SPAM!</PRE>

If you trace through this example's control flow, you'll see that we get both uppercase conversion (by inheritance) and HTML formatting (by composition), even though the core processing logic in the original Processor superclass knows nothing about either step. The processing code only cares that writers have a write method, and that a method named convert is defined; it doesn't care what those calls do. Such polymorphism and encapsulation of logic is behind much of the power of classes.

As is, the Processor superclass only gives a file-scanning loop. In more real work, we might extend it to support additional programming tools for its subclasses, and in the process turn it into a full-blown framework. By coding such tools once in a superclass, they can be reused in all your programs. Even in this simple example, because so much is packaged and inherited with classes, all we had to code here was the HTML formatting step; the rest is free.[1]

[1] For another example of composition at work, see this part's "Dead Parrot Sketch" exercise and solution; it's similar to the Pizza shop example.

Why You Will Care: Classes and Persistence

We've mentioned pickling a few times in this part of the book, because it works especially well with class instances. For instance, besides allowing us to simulate real-world interactions, the pizza shop classes could also be used as the basis of a persistent restaurant database. Instances of classes can be stored away on disk in a single step using Python's pickle or shelve modules. The object pickling interface is remarkably easy to use:

import pickle
object = someClass(  )
file   = open(filename, 'wb')  # Create external file.
pickle.dump(object, file)      # Save object in file.

import pickle
file   = open(filename, 'rb')
object = pickle.load(file)     # Fetch it back later.

Pickling converts in-memory objects to serialized byte streams, which may be stored in files, sent across a network, and so on; unpickling converts back from byte streams to identical in-memory objects. Shelves are similar, but automatically pickle objects to an access-by-key database, which exports a dictionary-like interface:

import shelve
object = someClass(  )
dbase  = shelve.open('filename')
dbase['key'] = object             # Save under key

import shelve
dbase  = shelve.open('filename')
object = dbase['key']             # Fetch it back later.

In our example, using classes to model employees means we can get a simple database of employees and shops with little extra work: pickling such instance objects to a file makes them persistent across Python program executions. See the standard library manual and later examples for more on pickling.


    [ Team LiB ] Previous Section Next Section