What is design pattern
In software engineering, a software design pattern is a general reusable solution to a commonly occurring problem within a given context in software design. It is not a finished design that can be transformed directly into source or machine code. It is a description or template for how to solve a problem that can be used in many different situations. Design patterns are formalized best practices that the programmer can use to solve common problems when designing an application or system.
Composite pattern
Composite pattern describes how group of similar objects can be treated by other components of the system in the same way as single object.
For example in a drawing program we have to deal with various shapes, lines, ellipses, rectangles, etc … The program will manage them in a uniform way (draw, move, rotate, …). But also, we want to group various shapes and manage the group in the same way as individual shapes.
The composite pattern describes a group of objects that is treated the same way as a single instance of the same type of object.
A single object is called a leaf, a group of similar objects objects is called a composite. Other components of the system (for example drawing program) that are using the composite objects is called the client.
Example 1
Description
Implement vector drawing program with various shapes and a feature to group the shapes into groups.
draw/shape.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<?php namespace draw; abstract class Shape { abstract public function draw(); abstract public function move(int $x, int $y); abstract public function resize(int $w, int $h); // add methods so that all shapes have the same interface public function add(Shape $shape) { throw new \Exception("Add not supported on the individual shape"); } public function remove(Shape $shape) { throw new \Exception("Add not supported on the individual shape"); } } |
All shapes have to implement methods draw(), move(), resize(). While methods add() and remove() will throw Exception. For individual shapes we will leave it like this, for groups we will override add() and remove() .
draw/rectangle.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
<?php namespace draw; class Rectangle extends Shape { protected $_x0; protected $_y0; protected $_x1; protected $_y1; public function __construct(int $x0, int $y0, int $x1, int $y1) { $this->_x0 = $x0; $this->_y0 = $y0; $this->_x1 = $x1; $this->_y1 = $y1; } public function draw() { print "Draw rectangle (" . $this->_x0 . "," . $this->_y0 . ") - (" . $this->_x1 . "," . $this->_y1 . ")\n"; } public function move(int $x, int $y) { print "Move rectangle (" . $this->_x0 . "," . $this->_y0 . ") - (" . $this->_x1 . ",." . $this->_y1 . ") to ($x, $y)\n"; } public function resize(int $w, int $h) { print "Resize rectangle (" . $this->_x0 . "," . $this->_y0 . ") - (" . $this->_x1 . ",." . $this->_y1 . ") to ($x, $y)\n"; } } |
draw/circle.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<?php namespace draw; class Circle extends Shape { protected $_x; protected $_y; protected $_r; public function __construct(int $x, int $y, int $r) { $this->_x = $x; $this->_y = $y; $this->_r = $r; } public function draw() { print "Draw circle (" . $this->_x . "," . $this->_y . ", " . $this->_r. ")\n"; } public function move(int $x, int $y) { print "Move circle (" . $this->_x . "," . $this->_y . ", " . $this->_r. ") to ($x, $y)\n"; } public function resize(int $w, int $h) { print "Resize circle (" . $this->_x . "," . $this->_y . ", " . $this->_r. ") to ($x, $y)\n"; } } |
draw/shapegroup.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
<?php namespace draw; class ShapeGroup extends Shape { protected $_shapes=array(); public function draw() { print "Composite:\n"; foreach($this->_shapes as $shape) { $shape->draw(); } } public function move(int $x, int $y) { print "Composite:\n"; foreach($this->_shapes as $shape) { $shape->move($y, $y); } } public function resize(int $w, int $h) { print "Composite:\n"; foreach($this->_shapes as $shape) { $shape->resize($w, $h); } } public function add(Shape $shape) { if(in_array($shape, $this->_shapes, true)) { return; } $this->_shapes[] = $shape; return $this; } public function remove(Shape $shape) { $ix = array_search($shape, $this->_shapes, true); if(false === $ix) { return; } array_splice($this->_shapes, $ix, 1, array()); return $this; } } |
ShapeGroup also extends Shape and implements same public methods as individual shape – so it can be used by the drawing program the same was as individual shapes. But it also implements add() and remove().
Test:
main.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?php spl_autoload_register(); $shapes=array(); $shapes[] = new Draw\Rectangle(20,20, 26, 30);; $shapes[] = new Draw\Circle(17,18, 40); $composite = new Draw\ShapeGroup(); $composite->add(new Draw\Circle(27,48, 14)); $composite->add(new Draw\Rectangle(13,14, 25, 27)); $shapes[] = $composite; foreach($shapes as $shape) { $shape->draw(); } |
output:
1 2 3 4 5 6 |
$ php -q main.php Draw rectangle (20,20) - (26,30) Draw circle (17,18, 40) Composite: Draw circle (27,48, 14) Draw rectangle (13,14) - (25,27) |
The drawing program can draw individual shapes and the groups in the uniform way and it doesn’t need to know which objects are groups and which are individual shapes.
But, there is a problem that needs to be solved: All classes in the pattern, the composite class ShapeGroup and individual shape classes ( Rectangle, Circle, …) should share the same interface. The individual shapes can not implement add() and remove() – so they throw an Exception. This is not the best solution since drawing program might unexpectedly trigger an error if it calls add() or remove() on object which is not composite.
This will throw exception:
main2.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?php spl_autoload_register(); $shapes=array(); $shapes[] = new Draw\Rectangle(20,20, 26, 30);; $shapes[] = new Draw\Circle(17,18, 40); $shapes[0] = $shapes[0]->add(new Draw\Circle(27,48, 14)); foreach($shapes as $shape) { $shape->draw(); } |
Output:
1 2 3 4 5 6 |
$ php -q main2.php PHP Fatal error: Uncaught Exception: Add not supported on the individual shape in /home/damir/.../draw/shape.php:14 Stack trace: #0 /home/damir/.../main2.php(9): draw\Shape->add(Object(draw\Circle)) #1 {main} thrown in /home/damir/.../draw/shape.php on line 14 |
So, the client needs to be aware which objects are the individual shapes and which are groups of the shapes:
main3.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<?php spl_autoload_register(); $shapes=array(); $shapes[] = new Draw\Rectangle(20,20, 26, 30);; $shapes[] = new Draw\Circle(17,18, 40); if($shapes[0] instanceof ShapeGroup) { $shapes[0] = $shapes[0]->add(new Draw\Circle(27,48, 14)); } else { $composite = new Draw\ShapeGroup(); $composite->add($shapes[0]); $composite->add(new Draw\Circle(27,48, 14)); $shapes[0] = $composite; } foreach($shapes as $shape) { $shape->draw(); } |
Using instanceof is considered bad OO programming practice and it is best avoided.
Example 2
Lets introduce new method for the abstract class Shape, getComposite() which will return NULL for single object (leaf) and for composite object it will return its reference.
draw/shape.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<?php namespace draw; abstract class Shape { abstract public function draw(); abstract public function move(int $x, int $y); abstract public function resize(int $w, int $h); public function getComposite() { return null; } // add methods so that all shapes have the same interface public function add(Shape $shape) { throw new \Exception("Add not supported on the individual shape"); } public function remove(Shape $shape) { throw new \Exception("Add not supported on the individual shape"); } } |
draw/shapegroup.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
<?php namespace draw; class ShapeGroup extends Shape { protected $_shapes=array(); public function draw() { print "Composite:\n"; foreach($this->_shapes as $shape) { print "..."; $shape->draw(); } } public function move(int $x, int $y) { print "Composite:\n"; foreach($this->_shapes as $shape) { print "..."; $shape->move($y, $y); } } public function resize(int $w, int $h) { print "Composite:\n"; foreach($this->_shapes as $shape) { print "..."; $shape->resize($w, $h); } } public function add(Shape $shape) { if(in_array($shape, $this->_shapes, true)) { return; } $this->_shapes[] = $shape; return $this; } public function remove(Shape $shape) { $ix = array_search($shape, $this->_shapes, true); if(false === $ix) { return; } array_splice($this->_shapes, $ix, 1, array()); return $this; } public function getComposite() { return $this; } } |
main3.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<?php spl_autoload_register(); $shapes=array(); $shapes[] = new Draw\Rectangle(20,20, 26, 30);; $shapes[] = new Draw\Circle(17,18, 40); $composite = $shapes[0]->getComposite(); if($composite !== null) { $shapes[0] = $composite->add(new Draw\Circle(27,48, 14)); } else { $composite = new Draw\ShapeGroup(); $composite->add($shapes[0]); $composite->add(new Draw\Circle(27,48, 14)); $shapes[0] = $composite; } foreach($shapes as $shape) { $shape->draw(); } |
In the above example responsibility which composite object to create is left to the client. In some situation this responsibility can be transferred to the leaf object it self, so getComposite() will create new composite object and add current object into the composite object, it will make code in the client more simple and straightforward.
Example 3
Change getComposite() in the abstract class Shape:
draw/shape.php
1 2 3 4 5 6 7 |
... public function getComposite() { $shapeGroup = new ShapeGroup(); return $shapeGroup->add($this); } ... |
main3.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?php spl_autoload_register(); $shapes=array(); $shapes[] = new Draw\Rectangle(20,20, 26, 30);; $shapes[] = new Draw\Circle(17,18, 40); $composite = $shapes[0]->getComposite(); $composite->add(new Draw\Circle(27,48, 14)); $shapes[0] = $composite; foreach($shapes as $shape) { $shape->draw(); } |
The client code is now more simple – but it lost the possibility to choose what kind of composite object to create. In this example it is ok since we are having only one composite object ShapeGroup.
Client can be simplified even more, by moving functionality of getComposite() to add():
Example 4
The add() and remove() methods for individual shapes will be refactored so that they will create new GroupShape object, add both the current object and the new object to the group and return the newly created composite object. The Shape class now looks like this:
draw/shape.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<?php namespace draw; abstract class Shape { abstract public function draw(); abstract public function move(int $x, int $y); abstract public function resize(int $w, int $h); public function add(Shape $shape) { $shapeGroup = new ShapeGroup(); $shapeGroup->add($this); $shapeGroup->add($shape); return $shapeGroup; } public function remove(Shape $shape) { if($this === $shape) { $shapeGroup = new ShapeGroup(); return $shapeGroup; } return $this; } } |
add() is straightforward.
remove() might look weird, but this is how it works: if client tries to remove the shape from itself empty
ShapeGroup is created and returned, otherwise the current object is returned. In both cases the object that is being removed from the group is not part of the group any more.
(Other classes remain unchanged.)
Test:
main3.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?php spl_autoload_register(); $shapes=array(); $shapes[] = new Draw\Rectangle(20,20, 26, 30);; $shapes[] = new Draw\Circle(17,18, 40); $shapes[0] = $shapes[0]->add(new Draw\Circle(27,48, 14)); foreach($shapes as $shape) { $shape->draw(); } |
Output:
1 2 3 4 5 6 |
$ php -q main3.php Composite: ...Draw rectangle (20,20) - (26,30) ...Draw circle (27,48, 14) Draw circle (17,18, 40) [damir@buffy composite]$ |
As you can see from example above – drawing program now can treat composite object ShapeGroup in the same way as individual shapes. It doesn’t need to know which object are of type ShapeGroup even when a calling add() to remove().
Conclusion
With composite pattern it is possible to create single objects and composite objects that will be treated in the uniform way by the client.
Only exception are add/remove methods: If a client wants to decide composite objects to create – it needs some way to tell apart single objects from composite objects. If this responsibility can be transferred to the individual object, then single objects and composite objects can be treated in uniform way even for add/remove methods.