Chapter 6. Assembling the Whole from the Parts with the Composite

When I was about 11 or 12 years old, I developed a theory of the universe. You see, I had just learned about the solar system and about atoms, and the similarities between the two seemed to be more than a coincidence. While our solar system has planets whirling around the sun, the atom (at least when you learn about it in elementary school) has electrons spinning around its nucleus. I remember wondering whether our whole world wasn’t just an electron in some larger universe. And maybe there was some fantastically tiny adolescent living out his whole life in a world that was part of the last atom at the tip of my pencil.

Although my theory almost certainly involved faulty physics, the idea of things being built of similar sub-things is actually a powerful one that you can apply to writing programs. In this chapter, we will look at the Composite pattern—a pattern that suggests that we build up bigger objects from small sub-objects, which might themselves be made up of still smaller sub-sub-objects. We will see how we can use the Composite pattern to model situations as diverse as organizational structures and the layout of graphical user interfaces. And who knows, in the unlikely event that my old theory of the universe proves correct, we might just be able to use the Composite pattern as a Model of Everything.

The Whole and the Parts

Building object-oriented software is the process of taking relatively simple objects, such as integers and strings, and combining them into slightly more complex objects, such as personnel records and song lists. We can then use the resulting objects to build even more interesting objects. What usually emerges at the end of this process are a few very sophisticated objects that look nothing like the component bits and pieces that we used to assemble them.

But not always. Sometimes we want a complex object to look and act exactly like the components we use to build it. Imagine you are working for TasteeBite Ltd., a gourmet bakery. You have been asked to build a system that keeps track of the manufacturing of the new ChocOBite chocolate cake. A key requirement is that your system must be able to keep track of the time it takes to manufacture a cake. Of course, making a cake is a fairly complicated process. First you have to make the cake batter, put the batter in a pan, and put the pan in the oven. Once the cake is baked, you need to frost it and package it for sale. Making the batter is also a reasonably complicated process in itself, involving a fairly long sequence of steps such as measuring out the flour, cracking the eggs, and licking the spoon.

As you can see from Figure 6-1, you can think of the cake-baking process as a tree, where the master task of making a cake is built up from subtasks such as baking the cake and packaging it, which are themselves made up of even simpler tasks.

Figure 6-1. The tree of cake-making tasks

image

Naturally, you do not want to subdivide the cake manufacturing process infinitely (“Now move the 1,463,474th speck of flour to the bowl . . .”). Instead, you need to identify the lowest-level, most-fundamental tasks of cake making and stop there. It is probably reasonable to stop at the “add dry ingredients” and “put cake in oven” level and model each of these steps as a separate class. Clearly, all of classes will need to share a common interface—an interface that will let them report back how much time they take. In your TasteeBite project, you decide to use a common base class, called Task, and you start by creating a bunch of subclasses, one for the each basic job: AddDryIngredientsTask, AddLiquidsTask, and MixTask.

So much for the simple tasks. But what about the more complex tasks, such as “make batter” or even “manufacture cake,” which are built up of smaller subtasks? On the one hand, these are perfectly respectable tasks. In exactly the same way that we might want to know how long it takes to do something basic such as add the dry ingredients, we might want to know how long it takes to make the batter, package the cake, or even make the whole cake. But, these higher-level tasks are not quite the same as the simple ones: They are built up from other tasks.

Obviously, you will need some kind of container object to deal with these complex (or shall we say, composite) tasks. But there is one other point to keep in mind about these higher-level tasks: While they are built up internally of any number of subtasks, from the outside they look just like any other Task.

This approach works on two levels. First, any code that uses the MakeBatterTask object does not have to worry about the fact that making batter is more complex than, say, measuring flour. Simple or complex, everything is just a Task. The same is true for the MakeBatter class itself: MakeBatter does not have to concern itself with the details of its subtasks; simple or complex, they are just Tasks to MakeBatter. That brings us to the second elegant aspect of this technique: Because MakeBatter simply manages a list of Tasks, any of those subtasks could themselves be made up of sub-subtasks. We can, in short, make the tree of tasks and subtasks go as deep as we like.

It turns out that this situation, in which you group together a number of components to create a new super-component, actually occurs fairly frequently. Think about how large companies are organized. Any company is fundamentally made up of people—the executives and the accountants and the factory workers. But we rarely think of a big company as simply a collection of individuals. Instead, we might look at a big company and see a conglomeration of divisions, which are made of departments, which are assembled from teams, which are staffed by people.

Departments and divisions have a lot in common with individual workers. For instance, all of them cost the company money in salaries: Fred in shipping might be grossly underpaid, for example, but so is the whole public relations department. Both workers and departments report to someone: Sally is Fred’s boss, while Lori is the vice president of the entire public relations department. Finally, both workers and whole departments can leave the company: Fred might find a better-paying job, while the public relations department could be sold off to a different firm. In terms of modeling, the people in a big company are analogous to the simple steps in cake baking, while the departments and divisions are higher-level elements akin to the more complex tasks involved in making a cake.

Creating Composites

The GoF called the design pattern for our “the sum acts like one of the parts” situation the Composite pattern. You will know that you need to use the Composite pattern when you are trying to build a hierarchy or tree of objects, and you do not want the code that uses the tree to constantly have to worry about whether it is dealing with a single object or a whole bushy branch of the tree.

To build the Composite pattern (Figure 6-2), you need three moving parts. First, you need a common interface or base class for all of your objects. The GoF call this base class or interface the component. Ask yourself, “What will my basic and higher-level objects all have in common?” In baking cakes, both the simple task of measuring flour and the much more complex task of making the batter take a certain amount of time.

Figure 6-2. The Composite pattern

image

Second, you need one or more leaf classes—that is, the simple, indivisible building blocks of the process. In our cake example, the leaf tasks were the simple jobs, such as measuring flour or adding eggs. In our organization example, the individual workers were the leaves. The leaf classes should, of course, implement the Component interface.

Third, we need at least one higher-level class, which the GoF call the composite class. The composite is a component, but it is also a higher-level object that is built from subcomponents. In the baking example, the composites are the complex tasks such as making the batter or manufacturing the whole cake—that is, the tasks that are made up of subtasks. For organizations, the composite objects are the departments and divisions.

To make this discussion a little more concrete, let’s look at the process of making a cake in terms of some code. We’ll start with the component base class:


   class Task
     attr_reader :name
  
     def initialize(name)
       @name = name
     end
  
     def get_time_required
       0.0
     end
   end


Task is an abstract base class in the sense that it is not really complete: It just keeps track of the name of the task and has a do-nothing get_time_required method.

Now let’s build two leaf classes:


   class AddDryIngredientsTask < Task
  
     def initialize
       super('Add dry ingredients')
     end
  
     def get_time_required
       1.0             # 1 minute to add flour and sugar
     end
   end
  
   class MixTask < Task
  
     def initialize
       super('Mix that batter up!')
     end
  
     def get_time_required
       3.0             # Mix for 3 minutes
     end
   end


The AddDryIngredientsTask and MixTask classes are very simple subclasses of Task; they simply supply a real get_time_required method. Obviously, we could go on and on defining all of the basic cake-baking tasks, but let’s just use our imagination and skip ahead to the punch line—a composite task:


   class MakeBatterTask < Task
     def initialize
       super('Make batter')
       @sub_tasks = []
       add_sub_task( AddDryIngredientsTask.new )
       add_sub_task( AddLiquidsTask.new )
       add_sub_task( MixTask.new )
     end
  
      def add_sub_task(task)
       @sub_tasks << task
     end
  
     def remove_sub_task(task)
       @sub_tasks.delete(task)
     end
  
     def get_time_required
       time=0.0
       @sub_tasks.each {|task| time += task.get_time_required}
       time
     end
   end


While the MakeBatterTask class looks to the outside world like any other simple task—it implements the key get_time_required method—it is actually built up from three subtasks: AddDryIngredientsTask and MixTask, which we saw earlier, plus AddLiquidsTask, a task whose implementation I will leave to your imagination. A key part of MakeBatterTask is the way it handles the get_time_required method. Specifically, MakeBatterTask totals up all of the times required by its child tasks.

Because we will have a number of composite tasks in our baking example (packaging the cake as well as the master task of manufacturing the cake), it makes sense to factor out the details of managing the child tasks into another base class:


   class CompositeTask < Task
     def initialize(name)
       super(name)
       @sub_tasks = []
     end
  
     def add_sub_task(task)
       @sub_tasks << task
     end
  
     def remove_sub_task(task)
       @sub_tasks.delete(task)
     end
  
     def get_time_required
       time=0.0
       @sub_tasks.each {|task| time += task.get_time_required}
       time
     end
   end


Our MakeBatterTask then reduces to the following code:


   class MakeBatterTask < CompositeTask
     def initialize
       super('Make batter')
       add_sub_task( AddDryIngredientsTask.new )
       add_sub_task( AddLiquidsTask.new )
       add_sub_task( MixTask.new )
     end
   end


The key point to keep in mind about composite objects is that the tree may be arbitrarily deep. While MakeBatterTask goes down only one level, that will not be true in general. For example, when we really finish out our bakery project, we will need a MakeCake class:


   class MakeCakeTask < CompositeTask
     def initialize
       super('Make cake')
       add_sub_task( MakeBatterTask.new )
       add_sub_task( FillPanTask.new )
       add_sub_task( BakeTask.new )
       add_sub_task( FrostTask.new )
       add_sub_task( LickSpoonTask.new )
     end
   end


Any one of the subtasks of MakeCakeTask might be a composite. In fact, we have already seen that MakeBatterTask actually is a composite.

Sprucing Up the Composite with Operators

We can make our composite code even more readable if we realize that composite objects fulfill a dual role. On the one hand, the composite object is a component; on the other hand, it is a collection of components. As we have written it, our CompositeTask does not look much like any of the standard Ruby collections, such as Array or Hash. It would be nice, for example, to be able to add tasks to a CompositeTask with the << operator, just as we could in an array:


   composite = CompositeTask.new('example')
   composite << MixTask.new


It turns out that we can get this done by simply renaming the add_sub_task method:


   def <<(task)
     @sub_tasks << task
   end


We might also want to get at our subtasks by the familiar array indexing syntax, something like this:


   puts(composite[0].get_time_required)
   composite[1] = AddDryIngredientsTask.new


This is once again a simple matter of picking the right name for our method. Ruby will translate object[i] into a call to a method with the odd name of [], which takes one parameter, the index. To add support for this operation to our CompositeTask class, we simply add a method:


   def [](index)
     @sub_tasks[index]
   end


In the same way, object[i] = value is translated into a call to the method with the even stranger name of []=, which takes two parameters, the index and the new value:


   def []=(index, new_value)
     @sub_tasks[index] = new_value
   end


An Array as a Composite?

We could also get the same container operator effect with our CompositeTask by simply making it a subclass of Array instead of Task:


   class CompositeTask < Array
     attr_reader :name
  
     def initialize(name)
       @name = name
     end
  
     def get_time_required
       time=0.0
       each {|task| time += task.get_time_required}
       time
     end
   end


Given the dynamic typing rules of Ruby, this approach will work. By making CompositeTask a subclass of Array, we get a container and its associated [], []=, and << operators for free via inheritance. But is this a good approach?

I vote no. A CompositeTask is not some specialized kind of array; it is a specialized kind of Task. If CompositeTask is going to be related to any class, it should be related to Task, not Array.

An Inconvenient Difference

Any implementation of the Composite pattern needs to deal with one other sticky issue. We began by saying that the goal of the Composite pattern is to make the leaf objects more or less indistinguishable from the composite objects. I say “more or less” here because there is one unavoidable difference between a composite and a leaf: The composite has to manage its children, which probably means that it needs to have a method to get at the children and possibly methods to add and remove child objects. The leaf classes, of course, really do not have any children to manage; that is the nature of leafyness.

How we handle this inconvenient fact is mostly a matter of taste. On the one hand, we can make the composite and leaf objects different. For example, we can supply the composite object with add_child and remove_child methods (or the equivalent array-like operators) and simply omit these methods from the leaf class. This approach has certain logic behind it: After all, leaf objects are childless, so they do not need the child management plumbing.

On the other hand, our main goal with the Composite pattern is to make the leaf and composite objects indistinguishable. If the code that uses your composite has to know that only some of the components—the composite ones—have get_child and add_child methods while other components—the leaves—do not, then the leaf and composite objects are not the same. But if you do include the child-handling methods in the leaf object, what happens if someone actually calls them on a leaf object? Responding to remove_child is not so bad—leaf objects do not have children so there is never anything to remove. But what if someone calls add_child on a leaf object? Do you ignore the call? Throw an exception? Neither response is very palatable.

As I say, how you handle this decision is mostly a matter of taste: Make the leaf and composite classes different, or burden the leaf classes with embarrassing methods that they do not know how to handle. My own instinct is to leave the methods off of the leaf classes. Leaf objects cannot handle child objects, and we may as well admit it.

Pointers This Way and That

So far, we have looked at the Composite pattern as a strictly top-down affair. Because each composite object holds references to its subcomponents but the child components do not know a thing about their parents, it is easy to traverse the tree from the root to the leaves but hard to go the other way.

It is easy to add a parent reference to each participant in the composite so that we can climb back up to the top of the tree. The best place to put the parent-reference handling code is in the component class. You can centralize the code to handle the parent there:


   class Task
     attr_accessor :name, :parent
  
     def initialize(name)
       @name = name
       @parent = nil
     end
  
     def get_time_required
       0.0
     end
   end


Given that the composite class is the place where the parent–child relationships are managed, it is also the logical place to set the parent attribute:


   class CompositeTask < Task
     def initialize(name)
       super(name)
       @sub_tasks = []
     end
  
     def add_sub_task(task)
       @sub_tasks << task
       task.parent = self
     end
  
     def remove_sub_task(task)
       @sub_tasks.delete(task)
       task.parent = nil
     end
  
     def get_time_required
       time=0.0
       @sub_tasks.each {|task| time += task.get_time_required}
       time
     end
   end


With the parent references in place, we can now trace any composite component up to its ultimate parent:


   while task
     puts("task: #{task}")
     task = task.parent
   end


Using and Abusing the Composite Pattern

The good news about the Composite pattern is that there is only one really common mistake people make when they implement it; the bad news is that they make this mistake a lot. The error that crops up so frequently with the Composite pattern is assuming that the tree is only one level deep—that is, assuming that all of the child components of a composite object are, in fact, leaf objects and not other composites. To illustrate this misstep, imagine we needed to know how many leaf steps are involved in our cake-baking process. We could simply add the following code to the CompositeTask class:


   #
   # Wrong way of doing it
   #
   class CompositeTask < Task
  
     # Lots of code omitted...
  
     def total_number_basic_tasks
       @sub_tasks.length
     end
  
   end


We could do that, but it would be wrong. This implementation ignores the fact that any one of those subtasks could itself be a huge composite with many of its own sub-subtasks. The right way to handle this situation is to define the total_num_basic_tasks method in the component class:


   class Task
     # Lots of code omitted...
  
     def total_number_basic_tasks
       1
     end
  
   end


Next we override this method in the composite class to run recursively down the tree:


   class CompositeTask < Task
  
     # Lots of code omitted...
  
     def total_number_basic_tasks
       total = 0
       @sub_tasks.each {|task| total += task.total_number_basic_tasks}
       total
     end
  
   end


Remember, the power of the Composite pattern is that it allows us to build arbitrarily deep trees. Do not go to all this trouble and then throw the advantage away by writing a few sloppy lines of code.

Composites in the Wild

If you look around for real examples of the Composite pattern in the Ruby code base, the graphical user interface (GUI) libraries jump out. All modern GUIs support a pallet of basic components, things like labels and text fields and menus. These basic GUI components have a lot in common with each other: No matter whether they are buttons or labels or menu items, all of them have a font, and a background and foreground color, and all of them take up a certain amount of screen real estate. Of course, any real, modern GUI is not just a simple collection of the basic GUI components. No, a real GUI is built as a hierarchy: Start with a label and a field, position them just so, and then join them together into a single visual thing that prompts the user for his or her first name. Combine this first-name prompter with a similar last-name prompter and a prompter that asks for a Social Security number. Combine those elements into a still larger and more complex GUI component. If you have read this chapter carefully, this process should sound very familiar: We have just built a GUI composite.

A good example of the use of composites in a GUI toolkit can be found in FXRuby. FXRuby is a Ruby extension that brings FOX—an open-source cross-platform GUI toolkit—to the Ruby world. FXRuby supplies you with a wide variety of user interface widgets, ranging from the mundane FXButton and FXLabel to the very elaborate FXColorSelector and FXTable. You can also build your own, arbitrarily complex widgets with FXHorizontalFrame and its cousin FXVerticalFrame. The two frame classes act as containers, allowing you to add sub-widgets to create a single unified GUI element. The difference between these two frame classes is the way they display their sub-widgets visually: One lines up the sub-widgets in a horizontal line, while the other stacks them vertically. Horizontal or vertical, both FOX frame widgets are subclasses of FXWindow, as are all of the basic widgets.

By way of example, here is an application that uses FXRuby to build a tiny, mock text editor:


   require 'rubygems'
   require 'fox16'
  
   include Fox
  
   application = FXApp.new("CompositeGUI", "CompositeGUI")
   main_window = FXMainWindow.new(application, "Composite",
                                  nil, nil, DECOR_ALL)
   main_window.width = 400
   main_window.height = 200
  
   super_frame = FXVerticalFrame.new(main_window,
                                     LAYOUT_FILL_X|LAYOUT_FILL_Y)
   FXLabel.new(super_frame, "Text Editor Application")
  
   text_editor = FXHorizontalFrame.new(super_frame,
                                       LAYOUT_FILL_X|LAYOUT_FILL_Y)
   text = FXText.new(text_editor, nil, 0,
          TEXT_READONLY|TEXT_WORDWRAP|LAYOUT_FILL_X|LAYOUT_FILL_Y)
   text.text = "This is some text."
  
   # Button bar along the bottom
  
   button_frame = FXVerticalFrame.new(text_editor,
                                      LAYOUT_SIDE_RIGHT|LAYOUT_FILL_Y)
  
   FXButton.new(button_frame,  "Cut")
   FXButton.new(button_frame, "Copy")
   FXButton.new(button_frame, "Paste")
  
   application.create
   main_window.show(PLACEMENT_SCREEN)
   application.run


The entire GUI is built as a series of nested composites. At the top of the tree is FXMainWindow, which has exactly one child element, a vertical frame. The frame has a horizontal frame for a child element, which has . . . Well, you get the picture—and if you don’t, have a look at Figure 6-3. It shows a neat Composite pattern that you can see on your screen.

Figure 6-3. A mock text editor, built with FXRuby

image

Wrapping Up

Once you grasp its recursive nature, the Composite pattern is really quite simple. Sometimes we need to model objects that naturally group themselves into larger components. These more complex objects fit into the Composite pattern if they share some characteristics with the individual components: The whole looks a lot like one of the parts. The Composite pattern lets us build arbitrarily deep trees of objects in which we can treat any of the interior nodes—the composites—just like any of the leaf nodes.

The Composite pattern is so fundamental that it is not surprising that it reappears, sometimes in disguise, in other patterns. As we will see in Chapter 15, the Interpreter pattern is nothing more than a specialization of the Composite pattern.

Finally, it is difficult to imagine the Composite pattern without the Iterator pattern. The reasons behind this hand-in-glove relationship are about to be revealed, because the Iterator pattern is the topic of the very next chapter.

..................Content has been hidden....................

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