Tuesday, October 20, 2009

Composite




















Chapter 6 -
Partitioning Patterns
Patterns in Java, Volume 1: A Catalog of Reusable Design Patterns Illustrated with UML, Second Edition
by Mark Grand
John Wiley & Sons � 2002



























Composite



The Composite pattern is also known as the Recursive Composition pattern. The Composite pattern was previously described in [GoF95].




Synopsis


The Composite pattern allows you to build complex objects by recursively composing similar objects in a tree-like manner. The Composite pattern also allows the objects in the tree to be manipulated in a consistent manner, by requiring all of the objects in the tree to have a common interface or superclass.


The following description of the Composite pattern describes it in terms of recursively building a composite object from other objects. The reason it appears in this partitioning patterns chapter is that during the design process the Composite pattern is often used to recursively decompose a complex object into simpler objects.






Context


Suppose that you are writing a document formatting program. It formats characters into lines of text organized into columns that are organized into pages. However, a document may contain other elements. Columns and pages can contain frames that can contain columns. Columns, frames, and lines of text can contain images. Figure 6.4 is a class diagram that shows these relationships.






Figure 6.4: Document container relationships.

As you can see, there is a fair amount of complexity here. Page and Frame objects must know how to handle and combine two kinds of elements. Column objects must know how to handle and combine three kinds of elements. The Composite pattern removes that complexity by allowing these objects to know how to handle only one kind of element. It accomplishes this by insisting that all document element classes implement a common interface. Figure 6.5 shows how you can simplify the document element class relationships by using the Composite pattern.






Figure 6.5: Document composite.

By applying the Composite pattern, you have introduced a common interface for all document elements and a common superclass for all container classes. Doing this reduces the number of aggregation relationships to one. Management of the aggregation is now the responsibility of the CompositeDocumentElement class. The concrete container classes (Document, Page, Column, etc.) only need to understand how to combine one kind of element.






Forces

















J



You have a complex object you want to decompose into a part-whole hierarchy of objects.




J



You want to minimize the complexity of the part-whole hierarchy by minimizing the number of different kinds of child objects that objects in the tree need to be aware of.




L



There is no requirement to distinguish between most of the part-whole relationships.








Solution



Minimize the complexity of a composite object organized into part-whole hierarchies by providing an interface to be implemented by all objects in the hierarchy and an abstract superclass for all composites in the hierarchy. The generalized class relationships for such an organization are shown in Figure 6.6.






Figure 6.6: Composite class relationships.

Here are descriptions of the interface and classes that participate in the Composite pattern:



ComponentIF.  An interface in the ComponentIF role is implemented by all the objects in the hierarchy of objects that make up a composite object. Composite objects normally treat the objects that they contain as instances of classes that implement the ComponentIF interface rather than as instances of their actual class.



Component1, Component2, and so on.  Instances of these classes are used as leaves in the tree organization.



AbstractComposite.  A class in this role is the abstract superclass of all composite objects that participate in the Composite pattern. AbstractComposite defines and provides default implementations of methods for managing a composite object’s components. The add method adds a component to a composite object. The remove method removes a component from a composite object. The getChild method returns a reference to a component object of a composite object.



ConcreteComposite1, ConcreteComposite2, and so on.  Instances of these are composite objects that use other instances of AbstractComponent.


Instances of these classes can be assembled in a treelike manner, as shown in Figure 6.7.






Figure 6.7: Composite object.

Note that you don’t need to have an abstract composite class if there is only one concrete composite class.






Implementation


If classes that participate in the Composite pattern implement any operations by delegating them to their parent object, then the best way to preserve speed and simplicity is to have each instance of AbstractComponent contain a reference to its parent. It is important to implement the parent pointer in a way that ensures consistency between parent and child. It must always be true that a ComponentIF object identifies an AbstractComposite object as its parent if, and only if, the AbstractComposite identifies it as one of its children. The best way to enforce this is to modify parent and child references only in the AbstractComposite class’s add and remove methods.


Sharing components among multiple parents using the Flyweight pattern is a way to conserve memory. However, shared components cannot easily maintain parent references.


The AbstractComposite class may provide a default implementation of child management for composite objects. However, it is very common for concrete composite classes to override the default implementation.


If a concrete composite object delegates an operation to the objects that constitute it, then caching the result of the operation may improve performance. If a concrete composite object caches the result of an operation, it is important that the objects that constitute the composite notify the composite object to invalidate its cached values.






Consequences





























J



You can access a tree-structured composite object and the objects that constitute it through the ComponentIF interface, whether they are simple objects or composite. The structure of the composite does not force other objects to make this distinction.




J



Client objects of an AbstractComponent can simply treat it as an AbstractComponent, without having to be aware of any subclasses of AbstractComponent.




J



If a client invokes a method of a ComponentIF object that is sup - posed to perform an operation and the ComponentIF object is an AbstractComposite object, then it may delegate the operation to the ComponentIF objects that constitute it. Similarly, if a client object calls a method of a ComponentIF object that is not an AbstractComposite and the method requires some contextual information, then the ComponentIF object delegates the request for contextual information to its parent.




l



Some components may implement operations that are unique to that component. For example, under the Context heading of this pattern is a design for the recursive composition of a document. At the lowest level, it has a document consisting of character and image elements. It is very reasonable for the character elements of a document to have a getFont method. A document’s image elements have no need for a getFont method. The main benefit of the Composite pattern is to allow the clients of a composite object and the objects that constitute it to be unaware of the specific class of the objects they deal with. To allow other classes to call getFont without being aware of the specific class they are dealing with, all the objects that can constitute a document can inherit the getFont method from DocumentElementIF. In general, when applying the Composite pattern, the interface in the ComponentIF role declares specialized methods if they are needed by any ConcreteComponent class.


 

A principle of object-oriented design is that specialized methods should appear only in classes that need them. Normally, a class should have methods that provide related functionality and form a cohesive set. This principle is essential to the Low Coupling/High Cohesion pattern described in Patterns in Java, Volume 2. Putting a specialized method in a general-purpose class rather than in the specialized class that needs the method is contrary to the principle of high cohesion. It is contrary to that principle because it adds a method unrelated to the other methods of the general-purpose class. The unrelated method is inherited by subclasses of the general- purpose class that are unrelated to the method.


 

Because simplicity through ignorance of class is the basis of the Composite pattern, when applying the Composite pattern it is okay to sacrifice high cohesion for simplicity. This exception to a widely accepted rule is based on experience rather than theory.




L



The Composite pattern allows any ComponentIF object to be a child of an AbstractComposite. If you need to enforce a more restrictive relationship, then you will have to add type-aware code to AbstractComposite or its subclasses. That reduces some of the value of the Composite pattern.








Java API Usage



The java.awt package contains an example of the Composite pattern. Its Component class fills the ComponentIF role. Its Container class fills the AbstractComposite role. It has a number of classes in the ConcreteComponent role, including Label, TextField, and Button. The classes in the ConcreteComposite role include Panel, Frame, and Dialog.






Code Example


The example of applying the Composite pattern is a more detailed version of the document-related classes that appeared under the Context heading. Figure 6.8 is a more detailed class diagram.



Figure 6.8 shows some methods that were left out of Figure 6.6. As you look through the following code, you will see that the setFont method is an example of a method that consults an object’s parent object. The getCharLength method gathers information from an object’s children and caches it for later use. The changeNotification method is used to invalidate cached information.






Figure 5.8: Detailed document composite.

There is also an abstract class named AbstractDocumentElement. This abstract class contains common logic for managing fonts and parents.


Here is code for the DocumentElementIF interface that all classes that make up a document must implement:


public interface DocumentElementIF { 
...

/**
* Return this object's parent or null if parentless.
*/
    public CompositeDocumentElement getParent() ;

/**
* Return the Font associated with this object.
*/
    public Font getFont() ;

/**
* Associate a Font with this object.
* @param font The font to associate with this object.
*/
    public void setFont(Font font) ;

/**
* Return the number of characters that this object
* contains.
*/
    public int getCharLength() ;
}  // interface DocumentElementIF

Here is a listing of the AbstractDocumentElement class that contains the common logic for managing fonts and parents:




abstract class AbstractDocumentElement
                                implements DocumentElementIF {
/**
* This is the font associated with this object. If the
* font variable is null, then this object's font will be
* inherited through the container hierarchy from an
* enclosing object.
*/
    private Font font;

/**
* This object's container
*/
    private CompositeDocumentElement parent;

...

/**
* Return this object's parent or null if it has no parent.
*/
    public CompositeDocumentElement getParent() {
        return parent;
} // getParent()

/**
* Set this object's parent.
*/
    protected void setParent(CompositeDocumentElement parent) {
        this.parent = parent;
} // setParent(AbstractDocumentElement)

/**
* Return the Font associated with this object. If
* there is no Font associated with this object, then
* return the Font associated with this object's parent.
* If there is no Font associated with this object's
* parent then return null.
*/
    public Font getFont() {
        if (font != null)
          return font;
        else if (parent != null)
          return parent.getFont();
        else
          return null;
} // getFont()
/**
     * Associate a Font with this object.
     * @param font
     * The font to associate with this object
*/
public void setFont(Font font) {
this.font = font;
    } // setFont(Font)

    /**
     * Return the number of characters that this object
     * contains.
     */
public abstract int getCharLength() ;
} // class AbstractDocumentElement


Here is the code for the CompositeDocumentElement class, which is the abstract superclass of all document elements that contain other document elements.




public abstract class CompositeDocumentElement
                             extends AbstractDocumentElement {
// Collection of this object's children
    private Vector children = new Vector();

/**
* The cached value from the previous call to getCharLength
* or -1 to indicate that charLength does not contain a
* cached value.
*/
    private int cachedCharLength = -1;

/**
* Return the child object of this object that is at the
* given position.
* @param index
* The index of the child.
*/
    public DocumentElementIF getChild(int index) {
        return (DocumentElementIF)children.elementAt(index);
} // getChild(int)

/**
* Make the given DocumentElementIF a child of this object.
*/
    public
    synchronized void addChild(DocumentElementIF child) {
        synchronized (child) {
            children.addElement(child);
            ((AbstractDocumentElement)child).setParent(this);
            changeNotification();
} // synchronized
    }  // addChild(DocumentElementIF)


The addChild and removeChild methods are both synchronized and also contain a synchronized statement to get a lock on the given child. This is because these methods modify both the container and its child.



    /**
* Make the given DocumentElementIF NOT a child of this
* object.
*/
public synchronized
    void removeChild(AbstractDocumentElement child) {
        synchronized (child) {
            if (this == child.getParent()) {
                child.setParent(null);
            }  // if
children.removeElement(child);
            changeNotification();
        } // synchronized
} // removeChild(AbstractDocumentElement)

...

/**
* A call to this method means that one of this object's
* children has changed in a way that invalidates whatever
* data this object may be caching about its children.
*/
public void changeNotification() {
        cachedCharLength = -1;
        if (getParent() != null)
          getParent().changeNotification();
    }  // changeNotification()

/**
* Return the number of characters that this object
* contains.
*/
    public int getCharLength() {
        int len = 0;
        for (int i = 0; i < children.size(); i++) {
            AbstractDocumentElement thisChild;
            thisChild
              = (AbstractDocumentElement)children.elementAt(i);
            len += thisChild.getCharLength();
        }  // for
        cachedCharLength = len;
        return len;
    } // getCharLength()
} // class CompositeDocumentElement


The Image class is an example of a class that implements a method so that the other classes that constitute a document do not need to be aware of the Image class as requiring any special treatment. Its getCharLength method always returns 1, so that an image can be treated as just a big character.



class Image extends Abstract DocumentElement {
...
    public int getCharLength() {
        return 1;
} // getCharLength()
} // class Image

The other classes in the class diagram that are subclasses of CompositeDocumentElement do not have any features that are interesting with respect to the Composite pattern. In the interest of brevity, just one of them is shown as follows.



class Page extends CompositeDocumentElement {
...
} // class Page





Related Patterns



Chain of Responsibility.  The Chain of Responsibility pattern can be combined with the Composite pattern by adding child to parent links so that children can get information from an ancestor without having to know which ancestor the information came from.



Low Coupling/High Cohesion.  The Low Coupling/High Cohesion pattern (described in Patterns in Java, Volume 2) discourages putting specialized methods in general-purpose classes, which is something that the Composite pattern encourages.



Visitor.  You can use the Visitor pattern to encapsulate operations in a single class that would otherwise be spread across multiple classes.


















No comments:

Post a Comment