Patterns For PHP
Index Patterns News Forums

Composite

From Patterns For PHP

This is article is a stub. You can help Patterns for PHP by expanding it.

A Composite a means of assembling and organising classes such that standalone objects and object collections are members of the same class hierarchy and may be used by client code without distinguishing between them.

Imagine for a moment a book. In a book, you'll find pages, chapters, cover-pages, subsections, prefaces, etc. Of course, we don't slop them all together and call it a book: the book is divided into chapters, which are then further divided into pages. An in-memory representation of a book composite might look like this:

Image:Book Composite.png

At first blush, it might seem logical to simply claim that a book only contains chapters, chapters only contain sections and sections only contain pages, and have each of the classes code around each of their unique interfaces. But there are common traits around all these elements, for example, they all contain text (or at least their children do). Standardize the interfaces around these common traits, and you've got a composite.

Contents

Example

Before we demonstrate the implementation of the pattern, let's look at how client code might utilise it. Let's take the example from our introduction a bit further. After implementing each of the Book, Chapter and Page classes we would expect our client code to utilise all these objects without distinguishing between them.

php
$book = new Book('Patterns For PHP');
$prologue = new Chapter('Prologue');
$chapter1 = new Chapter('Chapter 1: The Strategy Pattern');
$chapter2 = new Chapter('Chapter 2: The Registry Pattern');
 
// Add pages to all chapters
$prologue->addText( new Page('/home/user/pfp/page_prologue.html') );
$chapter1->addText( new Page('/home/user/pfp/page2_1.html') );
$chapter2->addText( new Page('/home/user/pfp/page3_1.html') );
 
// Create last page (stand-alone, has no parent chapter)
$lastPage = new Page('/home/user/pfp/page_last.html');
 
// Fill up the book
$book->addText($prologue);
$book->addText($chapter1);
$book->addText($chapter2);
$book->addText($lastPage);
 
$display = isset($_GET['c']) ? (int) $_GET['c'] : false;
if ($display !== false && $book->childExists($display_chapter)) {
    // Output only part of the book
    $book_child = $book->getChild($display);
    echo $book_child->output();
} else {
    // Output the book
    echo $book->output();
}

Notice how Book and Chapter both have addText() methods, rather than their own addChapter() and addPage() methods. This allows us to insert the standalone page, something not possible if it was assumed that all the children of a Book were chapters. Furthermore, all the classes in the hierarchy implement output(), allowing us to show the entire book, just one chapter, or even a single page.

Those familiar with the Document Object Model may note striking similarities between our example and the standard use-case of DOM. This is intentional: DOM is an extremely good example of the composite pattern. All elements in an XML document boil down to nodes, and they can be treated uniformly whether they are elements, text, or the document root node.

Implementation

This section is actively under discussion, see Talk:Composite.

Before we can actually start writing the code, there are a few common implementation issues we have to deal with first.

Child management operators

You may have noticed that in the above code, we never executed addText() on a page object. Since it's not a composite, such an operation is not meaningful. However, the whole point about composition is that you can treat components uniformly! Gang of Four boils it down to a trade-off between "safety and transparency". You can:

  1. Define child operations at the very bottom of the class hierarchy, so that even the non-composite objects have it. This lets you treat all objects uniformly, but lets the client do meaningless things like $marble->add($bag). In our case, however, we can implement an interesting fallback behavior: call output() on the passed node and then concatenate that to the existing page contents. This may not always be possible, however, and even when it is, it should be considered poor form.
  2. Define child operations only for Composites. While this means that it's now impossible for the client to add bag to the marble, it also means that the interfaces aren't uniform anymore.

The first possibility might look like this:

php
abstract class BookNode {
    // do nothing by default, we could also throw an exception
    public function addText() {}
    abstract function output();
    // all other standardized functions
}
abstract class BookComposite extends BookNode {
    private $texts = array();
    public function addText() {/* ... */}
    public function output() {/* ... */}
}
class Book extends BookComposite {/* ... */}
class Chapter extends BookComposite {/* ... */}
class Page extends BookNode {
    // we could keep the default behavior of doing nothing,
    // or we could implement the fallback behavior
    public function addText(/* ... */}
}

While the second possibility would look like this:

php
abstract class BookNode {
    abstract function output();
    // all other standardized functions
}
abstract class BookComposite extends BookNode {
    private $texts = array();
    public function addText() {/* ... */}
    public function output() {/* ... */}
}
class Book extends BookComposite {/* ... */}
class Chapter extends BookComposite {/* ... */}
class Page extends BookNode {/* ... */}

Throwing an "Attempt to call undefined function" error when addText is called on Page.

A rich base interface

The previous issue ties into a more general one, that is, how much should we stuff into the parent class? On one hand, anything we put into the base class is uniform across the entire hiearchy and can be used with impunity. On the other hand, it could mean a lot of junk methods that are meaningless for a particlar object in the hierarchy. The advice it to use good judgment and a little creativity for default behavior.

Code

So, here's an implementation of the book class mentioned above.

php
abstract class BookNode {
    abstract public function addText() {}
    abstract public function output() {}
    public function __toString() {
        return $this->output();
    }
}
abstract class BookComposite extends BookNode {
    protected $texts;
    public function addText($node) {
        $this->texts[] = $node;
    }
    public function output() {
        $ret = '';
        foreach ($texts as $text) {
            $ret .= $text->output();
        }
        return $ret;
    }
    public getChild($index) {
        if (isset($this->texts[$index])) return $this->texts[$index];
        // if we wanted to be nicer, we could
        // return a BookNodeNotFound class which
        // outputs a nice error message
        throw new Exception('Child doesn\'t exist!');
    }
}
class Book extends BookComposite
{
    protected $title;
    public function __construct($title) {
        $this->title = $title;
    }
    public function output() {
        $ret = "<h1>{$this->title}</h1>";
        $ret .= parent::output();
        return $ret;
    }
    function getChapter() 
}
class Chapter extends BookComposite
{
    protected $title;
    public function __construct($title) {
        // I know, it's a little duplication. If there
        // are a lot more titled objects added to the
        // hierarchy, you may want to consider
        // a BookTitledComposite abstract class.
        $this->title = $title;
    }
    public function output() {
        $ret = "<h2>{$this->title}</h2>";
        $ret .= parent::output();
        return $ret;
    }
}
class Page extends BookNode
{
    protected $contents = '';
    public function __construct($source_file) {
        $this->contents = file_get_contents($source_file);
        // we should check to make sure the source file exists
    }
    public function addText($node) {
        // convenience function
        if ($node instanceof BookNode) {
            $this->contents .= $node->output();
        } else {
            $this->contents .= (string) $node;
        }
    }
    public function output() {
        return $contents;
    }
}

Note that in this example the model and view are mixed together, which is generally considered a no-no. Find out how to seperate the HTML generation from these classes using a Visitor. You may also want to go through the book page by page, that would be implemented with an Iterator.

Retrieved from "/wiki/Composite"
MediaWiki
GNU Free Documentation License 1.2