Elementary Patterns
Strategy, Decorator, and Composite
The Java I/O Classes

Joseph Bergin
Pace University
jbergin@pace.edu

 

In this note we will examine three standard Design Patterns, Strategy, Decorator, and Composite. These are discussed in the book Design Patterns by Gamma, Helm, Johnson, and Vlissides. They are very useful for writing flexible programs and are widely used in the design of the Java library classes.

We will start with a few preliminaries.

Java Input of Text

Java uses a set of classes called Readers to read text from files. There are also Input Stream classes used for more general input. Text files are stored in a human readable form, sometimes called ASCII (American Standard Code for Information Interchange) but now becoming replaced with a more general international code called UNICODE that is supported by Java. If you want to read from a text file, use a reader.

Readers can be used to read from the keyboard or from disk files or other media, or to obtain information over the net. To read from a file you want to use a FileReader. Readers in general know how to translate from external encodings into internal (UNICODE) form. FileReaders simply add the ability to connect a Reader to a named file.

FileReader inputdata = new FileReader("mydata.txt");
int c = inputdata.read();

You read chars from Readers. We use an int variable, rather than a char, since a Reader will indicate end of file by returning a negative value. However, it is not very efficient to read a character at a time, since the reading mechanism from a disk is very slow compared to the speed of the CPU. Therefore it is advantageous to read a lot of characters at a time using a BufferedReader. This means that we do fewer reads, speeding up the program (immensely). Just as a FileReader is itself a reader, so is a BufferedReader, but it also has the ability to provide any other reader with the ability to buffer its input. We say that the BufferedReader "decorates" a reader with buffering ability.

BufferedReader inputData = new BufferedReader(new FileReader("mydata.txt"));
String buffer = inputData.readLine();

Note that most of these instructions may throw IOExceptions and so they are normally written in try blocks or put into functions that pass the exceptions on to their callers. We will deal with this later.

Here we decorate a new FileReader with a new BufferedReader. We can then get an entire line of text from the file in a single read.

StringTokenizer

Once we have a line of text read into our program we need to examine it to get the data we want. Sometimes we just want to process the individual characters in the string and can do this with either Enumerations over the string or with for loops or while loops.

However, often we really want to break up the string into pieces larger than a character. For example the string might contain English or Russian words or it might contain numbers separated by commas. For this purpose a (java.util) StringTokenizer is ideal, since it can break up the string for us, giving us the individual elements (called tokens) one at a time. We can specify how the tokens are separated when we create the tokenizer, or later if we like.

StringTokenizer words = new StringTokenizer(buffer, ",;");

Here we define a tokenizer named words that uses either commas or semicolons to divide its tokens. This assumes that the string named buffer has this structure, perhaps something like the following:

"123, 234, 345, 456, 567"

If you don't specify the delimiters then ordinary white space characters (space, tab, and newline) are used as defaults.

We can now use the tokenizer to extract the tokens of the string.

total = 0;
while(words.hasMoreTokens())
{	String num = words.nextToken();
	int val = Integer.parseInt(num);
	total += val;
}

Strategy Pattern

Suppose that we are given a problem to read a file of words (separated by the usual white space characters) and print out all of the words that begin with "t". Then suppose that we are given another problem to read a file of words and print out all the words longer than 5 characters. Then suppose we are to read a file of words and print out all the palindromes (words that spell the same thing if written backwards.

All of these programs have the same structure, so we could write one and copy it and make modifications to solve the others. We can also write it in such a way that we don't need to make modifications, but we just make it flexible enough to solve all three problems.

To do this we factor out what is variable about these problems from the rest and represent the variable part as a Strategy. The variable part here involves examining a word from the file and determining if it has a certain characteristic. We implement this strategy using an interface:

public interface CheckStrategy
{	public boolean check(String s);
}

We can then implement different checking strategies. For example, here are the three required above:

public class StartWithT implements CheckStrategy
{	public boolean check(String s)
	{	if( s == null || s.length() == 0) return false;
		return s.charAt(0) == 't';
	}
}

public class LongerThan5 implements CheckStrategy
{	public boolean check(String s)
	{	if(s == null) return false;
		return s.length() > 5;
	}
}

public class Palindrome implements CheckStrategy
{	public boolean check(String s)
	{	if(s == null) return false;
		int length = s.length();
		if(length < 2) return true;
		int half = length/2;
		for(int i = 0; i < half; ++i)
			if(s.charAt(i) != s.charAt(length - 1 - i)) return false;
		return true;
	}
}

We can now write the part of the program that doesn't change, but write it to use a strategy object to carry out the task of immediate interest. Here we will just write a method that processes a named file. Presumably this method is needed in some class that we are currently developing.

public void printWhen(String filename, CheckStrategy which) throws IOException
{	
	BufferedReader infile = new BufferedReader(new FileReader(filename));
	String buffer = null;
	while((buffer = infile.readLine()) != null)
	{	StringTokenizer words = new StringTokenizer(buffer);
		while( words.hasMoreTokens() )
		{	String word = words.nextToken();
			if (which.check(word)) System.out.println(word);
		}
	}
}

This method reads through the entire file a line at a time and then processes each line a word at a time. For each word it applies the check method of the strategy and prints the word if the strategy returns true. We can use this to print palindromes from a file with

printWhen("henry5.txt", new Palindrome());

We can make the above even more useful by parametrizing some of the strategies. For example, if we are checking for words longer than 5 characters, we might also want to check for words longer than 3. Here is a more general strategy.

Public class LongerThanN implements CheckStrategy
{	public LongerThanN(size n) {this.n = n;}
	private int n; 

	public boolean check(String s)
	{	if(s == null) return false;
		return s.length() > n;
	}
} 

With this strategy we can now find words longer than five characters with.

printWhen("henry5.txt", new LongerThanN(5));

There are many kinds of strategies. Here we just used a checking strategy. We could also employ a different kind of strategy that carried out different actions on objects depending on the implementation of the strategy. For example, in all of the above problems we were to print the words that met a criteria. There might be various possibilities for actions, however. These might include saving them in a Vector, encoding them with a Caesar Cypher and printing those results, and many others. Different kinds of strategies might be employed to write flexible programs that can solve a large class of problems, rather than a single one.

The key to a Strategy is to factor out what might be variable in a set of problems and build an interface for that. Then write the solution to the set of problems in terms of the interface. The interface is used as the parameter type of one or more methods in the class that solves the problem. A strategy might consist of one or more methods. These methods are called as appropriate to tailor the solution to the specific problem. When the general solution is to be reused in a new situation, the programmer just writes a new strategy for the new situation.

Note that Layout objects in the Java AWT implement a Strategy pattern. When you add a layout to a Panel, you give the panel a strategy for arranging the elments contained in the Panel.

Decorator Pattern

Decorators are used to provide additional functionality to an object of some kind. The key to a decorator is that a decorator "wraps" the object decorated and looks to a client exactly the same as the object wrapped. This means that the decorator implements the same interface as the object it decorates.

You can think of a decorator as a shell around the object decorated. Any message that a client sends to the object is caught by the decorator instead. The decorator may apply some action and then pass the message it received on to the decorated object. That object probably returns a value (to the decorator) which may again apply an action to that result, finally sending the (perhaps modified) result to the original client. To the client the decorator is invisible. It just sent a message and got a result. However the decorator had two chances to enhance the result returned.

For example, suppose that we would like to solve all of the above problems from the Strategy section, but we would also like to have a count of how many words we produced (how many times the strategy returned true). We could modify some of the above code to do this, of course, but would prefer not to. Since Strategy was defined by an interface we can design a decorator for strategies. A strategy decorator will take a strategy as a parameter in its constructor and will itself implement the strategy interface.

class CounterDecorator implements CheckStrategy
{	public CounterDecorator(CheckStrategy check)
	{	checker = check;
	}
	
	public boolean check(String s)
	{	boolean result =  checker.check(s);
		if(result)count++;
		return result;
	}
	
	public int count(){return count;}
	
	public void reset(){count = 0;}

	private int count = 0;
	private CheckStrategy checker;
}

Now we can find palindromes, print and count them with the following:

CounterDecorator counter = new CounterDecorator (new Palindrome());
printWhen("henry5.txt", counter);
System.out.println("" + counter.count());

Notice that the check method of the count decorator calls the check method of the palindrome method it is initialized with. It then counts the word if the result is true. Note that neither the palindrome class or the printWhen method had to be modified to achieve this result. This is the power of the decorator pattern.

Note also that decorators can provide additional methods beyond what is required by the interface that they decorate. In the current example, we have the count and reset methods of the CounterDecorator.

The key to a successful Decorator is to have a set of objects defined by an interface. The decorator implements that interface and takes an object of that interface type as a parameter of its constructor, which it saves internally. Then, when an interface message is sent to the decorator it passes sends the same message, or a related one, to the decorated object and gets the result. It may modify or replace the returned object as appropriate and return it to the original sender of the message.

Again, note that the Java I/O classes are mostly decorators. A BufferedReader is just a decorator for a Reader.

Another Dimension. Composite Pattern

Strategy lets us factor out the parts that change in a set of problems and Decorator lets us add functionality to an object like a strategy. Suppose, however, that we have a bunch of check strategies and we would like to find all of the words in some text file that satisfy all of our criteria. In otherwords, we want to apply all of our strategies and take the intersection of the results. We could write a new strategy with a complex test, or we could compose our strategies with a Composite.

A Composite is an object that does two things. First, it implements some interface, and second it is a container that contains things that implement the same interface. Then, when the composite object gets some message, it passes that message on to the other objects that it contains.

So, let's build a strategy composite to let us compose strategies using logical AND. It will implement CheckStrategy and contain CheckStrategies. We will use a Vector to hold the contained objects.

class AndStrategyComposite implements CheckStrategy
{	
	public void addStrategy(CheckStrategy s) { tests.addElement(s); }

	public boolean check(String s)
	{	java.util.Enumeration e = tests.elements();
		while (e.hasMoreElements())
		{	CheckStrategy strategy = (CheckStrategy)e.nextElement();
			if( ! strategy.check(s) ) return false;
		}
		return true;
	}

	private java.util.Vector tests = new Vector();
}

This strategy returns false if any of the strategies we have added to it return false for the same string. Otherwise it returns true. Note that it returns true if we have added no strategies to it at all.

We can now create a new AndStrategyComposite and add other strategies to it. We can then use this new object as a strategy.

...
AndStrategyComposite longWordsThatStartWithT = new AndStrategyComposite();
longWordsThatStartWithT.addStrategy(new LongerThan(5));
longWordsThatStartWithT.addStrategy(new StartWithT());
...
printWhen("henry5.txt", longWordsThatStartWithT);

And of course, since this is itself just a strategy, we could decorate it with a counter and get the count as well.

The key to a successful Composite is to have a container that implements an interface and it contains objects that also implement the same interface. When the composite gets a message defined in the interface it passes that message on to those objects it contains. We normally need to be able to add (and perhaps remove) elements.

The Java AWT Panel Component is built with the Composite Pattern. A Panel is a Component, but it also contains Components. When you tell a Panel to "paint" it paints the components it contains. You can add (and remove) components at any time.

Summary

We opened with a discussion of Java input. We then discussed both the strategy and decorator patterns. We hope that the reader realizes that many of the classes in the Java i/o library are just decorators. For example, BufferedReader is just a reader decorator. It has the same interface, plus a few more methods, as a Reader. Therefore, anywhere you can use any Reader, you can use a BufferedReader. Given this simple idea, the structure of the Java i/o libraries becomes quite comprehensible. Each class in the structure is designed to do one task. They work together by decorating other readers to achieve sophisticated results.

We saw a different method of composing functionality in the Composite pattern. Here we wanted to apply a set of existing operations in a well defined way. The composite is a sort of higher level strategy since it has a strategy of composing the things it contains. We used an AND strategy in our example.

You can use these same ideas in your programming.

Just so that you are not misled, we have decorated simple strategies here, but things can be much more general. We can actually decorate anything defined via an interface and we can also have much more interesting strategies. We can build composites over most interfaces.

The three patterns shown here are discussed in Design Patterns by Gamma, Helm, Johnson, and Vlissides (Addison-Wesley, 1995). This is the now famous "Gang of Four," or GOF book.

Using This In The Classroom

Eugene Wallingford (University of Northern Iowa) incorporated this material into his course in Object Oriented Programming. Here are the summaries of two of his lectures:

Session 27 (pdf file)

Session 28 (pdf file)

The Composite piece of this note is due to Eugene, by the way. I shamelessly stole it.

For completeness, Eugene does the Decorator pattern differently. Here is his treatment in Session 17 (pdf file).

Exercises

Parametrize the class StartWithT, renaming it, of course, to make it more useful.

Write a "saving decorator" that will save in a Vector all of the words from the input for which the decorated Strategy object returns true, and all the other words in another vector.

Write a boolean reversing decorator for our simple strategies that returns true when the Strategy object it decorates returns false, and conversely.

Write a logical OR composite for CheckStrategies. It should return true if any of its parts return true. Otherwise it should return false. What should be returned from an OR composite if you don't add any strategies to it?

Acknowledgements

As noted above, Eugene Wallingford had a big influence on this. Also Duane Buck of Otterbein College and Dorothy Nixon a graduate student at Queens College pointed out a few errors that have been fixed. Thanks.


Last Updated: March 5, 2010