Tuesday, December 13, 2011

The Composite Pattern

The Composite pattern defines a tree structure. A tree structure is a hierarchical composition of nodes, kind of a nested collection; a node can be composed of other nodes or not. Nodes that contain other nodes are called parent-nodes while the nodes they contain are not surprisingly called child-nodes. Some nodes are not or cannot be a parent to other nodes. These are called leaf-nodes. One benefit of the Composite pattern is that we can treat the entire tree, a branch (or sub-tree), or a single node equally since they're all the same data type, ie. nodes.
Graphically, the structure resembles a tree hence the name.


An obvious example would the be folders or directories on your hard drive, which can contain other folders, which can contain others and so on. Files would be our leaf-nodes since they cannot contain further folders. While searching for a file, your computer visits each folder and each file within each folder in a certain order until the desired file is found. The process of visiting each node is called tree traversal.
There are many uses for tree structures ranging from collision detection to list sorting algorithms.
In this article we will use the Composite pattern to set up a tree structure and learn how to traverse it in different ways.
Let's take a quick look at the class diagram.

As you can see, the Node which defines the interface that our client class will be working with contains methods for adding, removing and retrieving nodes. A leaf-node cannot contain other nodes but must still inherit from the Node interface so these methods are superfluous, moreover, there may be methods or properties that apply only to a leaf node and not a component node. While this does break the "one class, one responsibility" paradigm, we get the benefit of being able to treat every part of the tree the same: as a Node. This can make for somewhat dangerous code if the client tries accessing something that does not apply to the object but there are ways to handle it and the ability to treat each part of the tree uniformly can be worth the added overhead.
For our example we'll build a mock folder tree with "folders" and "files". The folders will have a name and be able to contain either other folders or files. The files will have a name and a "description" string field but will be unable to contain other nodes. Likewise, our folders will have no description field.
//The interface all nodes will implement.
interface IDirectoryNode
{
    string Name { get; }
    string Description { get; set; }
    void AddNode(IDirectoryNode node);
    void RemoveNode(IDirectoryNode node);
    IDirectoryNode[] GetNodes();
}

//Our Composite node class.
class Folder : IDirectoryNode
{
    string _name;
    List<IDirectoryNode> _nodes = new List<IDirectoryNode>();

    string Name
    {
        get { return _name; }
    }

    string Description
    {
        //This is one way of handling this.
        //we return a null description and the
        //setter does nothing.  We could have
        //thrown an exception among other solutions.
        get { return null; }
        set { }
    }

    public Folder(string name)
    {
        _name = name;
    }

    public void AddNode(IDirectoryNode node)
    {
        _nodes.Add(node);
    }

    public void RemoveNode(IDirectoryNode node)
    {
        _nodes.Remove(node);
    }

    public IDirectoryNode[] GetNodes()
    {
        return _nodes.ToArray();
    }
}

//Now for the leaf nodes.
class File : IDirectoryNode
{
    string _name;
    string _description;

    string Name
    {
        get { return _name; }
    }

    string Description
    {
        get { return _description; }
        set { _description = value; }
    }

    public File(string name, string description)
    {
        _name = name;
        _description = description;
    }

    //These do nothing.
    public void AddNode(IDirectoryNode node) { }
    public void RemoveNode(IDirectoryNode node) { }

    public IDirectoryNode[] GetNodes()
    {
        return null;
    }
}
This is good is the sense that we can create Folders and Files and build our tree but there's not much we can do with it from there. We need a ways to traverse the tree. There are two ways to do this: with a depth-first search where we start at a node and visit all of its children before backtracking or a breadth-first search where we visit all the nodes on the same level before exploring their children.
We will implement both as a way to search for names and descriptions.
First we'll make methods that perform a depth-first search and return a list of nodes that match what we're looking for. We'll be using recursion in this example but there are iterative ways to do it as well. Let's add these new search methods to our interface and classes.
interface IDirectoryNode
{
    string Name { get; }
    string Description { get; set; }
    void AddNode(IDirectoryNode node);
    void RemoveNode(IDirectoryNode node);
    IDirectoryNode[] GetNodes();
    List<IDirectoryNode> SearchForNames(string name);
    List<IDirectoryNode> SearchForDescriptions(string descriptions);
}

class Folder : IDirectoryNode
{
    string _name;
    List&lt;IDirectoryNode&gt; _nodes = new List&lt;IDirectoryNode&gt;();

    string Name
    {
        get { return _name; }
    }

    string Description
    {
        get { return null; }
        set { }
    }

    public Folder(string name)
    {
        _name = name;
    }

    public void AddNode(IDirectoryNode node)
    {
        _nodes.Add(node);
    }

    public void RemoveNode(IDirectoryNode node)
    {
        _nodes.Remove(node);
    }

    public IDirectoryNode[] GetNodes()
    {
        return _nodes.ToArray();
    }

    public List<IDirectoryNode> SearchForNames(string name)
    {
        List<IDirectoryNode> results = new List<IDirectoryNode>();
        foreach (IDirectoryNode node in _nodes)
        {
            string nodeName = node.Name;

            //Some debugging to make sure we're traversing
            //the tree in the correct order.
            Console.WriteLine(
                "Visiting " + nodeName + " " + "the " + node.ToString());

            if (nodeName == name)
            {
                results.Add(node);
            }

            //Here is where we recurse down into the tree.
            results.AddRange(node.SearchForNames(name));
        }
        return results;
    }

    public List<IDirectoryNode> SearchForDescriptions(string description)
    {
        List<IDirectoryNode> results = new List<IDirectoryNode>();

        foreach (IDirectoryNode node in _nodes)
        {
            string nodeDescription = node.Description;

            //Some debugging to make sure we're traversing
            //the tree in the correct order.
            Console.WriteLine(
                "Visiting " + node.Name + " " + "the " + node.ToString());

            if (nodeDescription != null &&
                nodeDescription == description)
            {
                results.Add(node);
            }

            results.AddRange(node.SearchForDescriptions(description));
        }

        return results;
    }
}

class File : IDirectoryNode
{
    string _name;
    string _description;

    string Name
    {
        get { return _name; }
    }

    string Description
    {
        get { return _description; }
        set { _description = value; }
    }

    public File(string name, string description)
    {
        _name = name;
        _description = description;
    }

    //These do nothing.
    public void AddNode(IDirectoryNode node) { }
    public void RemoveNode(IDirectoryNode node) { }

    public IDirectoryNode[] GetNodes()
    {
        return null;
    }

    //These are not applicable so we just
    //return an empty list.
    public List<IDirectoryNode> SearchForNames(string name)
    {
        return new List<IDirectoryNode>();
    }

    public List<IDirectoryNode> SearchForDescriptions(string description)
    {
        return new List<IDirectoryNode>();
    }
}
Let's test this out by building a tree and searching for some things.
class Program
{
    static void Main(string[] args)
    {
        //Building our tree...
        IDirectoryNode documentsFolder = new Folder("Documents");
        IDirectoryNode gamesFolder = new Folder("Games");
        IDirectoryNode musicFolder = new Folder("Music");
        IDirectoryNode someGame = new File("Fun Time Game", "Whooo, fun!");
        IDirectoryNode someSong = new File("Happy Song", "La la la");
        IDirectoryNode textDocument = new File("Journal", "I had a good day!");
        IDirectoryNode textDocumentTwo = new File("Journal", "I had a bad day!");

        documentsFolder.AddNode(gamesFolder);
        documentsFolder.AddNode(musicFolder);
        documentsFolder.AddNode(textDocument);
        gamesFolder.AddNode(someGame);
        musicFolder.AddNode(someSong);
        musicFolder.AddNode(textDocumentTwo);

        //Now we can do searches.
        List<idirectorynode> nameResults = new List<idirectorynode>();
        nameResults = 
            documentsFolder.SearchForNames("Journal");

        foreach (IDirectoryNode node in nameResults)
        {
            Console.WriteLine(node.Name);
        }

        Console.WriteLine();

        List<idirectorynode> descriptionResults = 
            documentsFolder.SearchForDescriptions("La la la");

        foreach (IDirectoryNode node in descriptionResults)
        {
           Console.WriteLine(node.Name);
        }

        Console.ReadLine();
   }
}
As you can see, each child is being completely explored before moving on to the next.
Now let's try to make the breadth-depth search.
We'll be using a queue to keep track of the nodes we still need to explore. Once a node in the queue has been explored, it's removed from line and its children are added. This is repeated until the queue is empty.
This image from wikipedia illustrates the algorithm beautifully.


The grey nodes are the ones currently enqueued and the black nodes are the ones being visited.
For the sake of brevity I will write out just search methods. The interface needs the new stubs as well.
//The breadth-first Folder search implementation.
public List<IDirectoryNode> BreadthFirstNameSearch(string name)
{
    List<IDirectoryNode> result = new List<IDirectoryNode>();
    Queue<IDirectoryNode> searchQueue = new Queue<IDirectoryNode>();

    foreach (IDirectoryNode node in _nodes)
    {
        searchQueue.Enqueue(node);
    }

    while (searchQueue.Count != 0)
    {
        IDirectoryNode currentNode = searchQueue.Dequeue();

        Console.WriteLine(
            "Examining " + currentNode.ToString() + ", " + currentNode.Name);

        if (currentNode.Name == name)
        {
            result.Add(currentNode);                   
        }
        foreach (IDirectoryNode node in currentNode.GetNodes())
        {
            searchQueue.Enqueue(node);
        }
    }

    return result;
}

public List<IDirectoryNode> BreadthFirstDescriptionSearch(string description)
{
    List<IDirectoryNode> result = new List<IDirectoryNode>();
    Queue<IDirectoryNode> searchQueue = new Queue<IDirectoryNode>();

    foreach (IDirectoryNode node in _nodes)
    {
        searchQueue.Enqueue(node);
    }

    while (searchQueue.Count != 0)
    {
        IDirectoryNode currentNode = searchQueue.Dequeue();

        Console.WriteLine(
            "Examining " + currentNode.ToString() + ", " + currentNode.Name);


        if (currentNode.Description != null &&
            currentNode.Description == description)
        {
            result.Add(currentNode);                   
        }

        foreach (IDirectoryNode node in currentNode.GetNodes())
        {
            searchQueue.Enqueue(node);
        }
    }

    return result;
}
Since we cannot search files their implementations simply return empty lists.
If we had used a stack instead of a queue this would have resulting in a depth-first search. Testing this out:
class Program
{
    static void Main(string[] args)
    {
        IDirectoryNode documentsFolder = new Folder("Documents");
        IDirectoryNode gamesFolder = new Folder("Games");
        IDirectoryNode musicFolder = new Folder("Music");
        IDirectoryNode someGame = new File("Fun Time Game", "Whooo, fun!");
        IDirectoryNode someSong = new File("Happy Song", "La la la");
        IDirectoryNode textDocument = new File("Journal", "I had a good day!");
        IDirectoryNode textDocumentTwo = new File("Journal", "I had a bad day!");

        documentsFolder.AddNode(gamesFolder);
        documentsFolder.AddNode(musicFolder);
        documentsFolder.AddNode(textDocument);
        gamesFolder.AddNode(someGame);
        musicFolder.AddNode(someSong);
        musicFolder.AddNode(textDocumentTwo);

        List<IDirectoryNode> nameResults = new List<IDirectoryNode>();
        nameResults = 
            documentsFolder.BreadthFirstNameSearch("Journal");

        foreach (IDirectoryNode node in nameResults)
        {
            Console.WriteLine(node.Name);
        }

        Console.WriteLine();

        List<IDirectoryNode> descriptionResults = 
            documentsFolder.BreadthFirstDescriptionSearch("La la la");

        foreach (IDirectoryNode node in descriptionResults)
        {
            Console.WriteLine(node.Name);
        }

        Console.ReadLine();
    }
}


Notice the different order in which each node is visited.
This is just one very simple way to do this, there are more sophisticated and generalized ways of going about this but the algorithms themselves are the same or very similar.

And this concludes my Design Pattern series (for now)! Next I'll start focusing more on collision algorithms and game physics.
I hope you've enjoyed reading them as much as I have writing them!
Feel free to leave comments, questions, corrections or what have you. Thanks!

No comments :

Post a Comment