So far, you've had the chance to read about the theory of the Decorator pattern with an extremely simple example. In this section, you'll have the chance to see a slightly more sophisticated and practical example that makes use of the Decorator pattern. We'll build a group of classes that work together for reading text data in many different ways. The reader example starts with a concrete decorated type that takes a string value and reads it one character at a time. Then, we'll add decorators that can read from the string by the word and by the line. We'll then add additional decorated and decorator types to demonstrate how they can be used interchangeably. Creating the Decorator/Decorated InterfaceTo implement the Decorator pattern, we first need an interface. For this example, we'll define an interface called com.peachpit.aas3wdp.decoratorexample.io.IReader. All reader decorated and decorator classes must implement this interface. For our example, we'll require that all reader types should be capable of dispatching events. For this reason IReader extends flash.events.IEventDispatcher. Here's our interface: package com.peachpit.aas3wdp.decoratorexample.io { import flash.events.IEventDispatcher; public interface IReader extends IEventDispatcher { function read():String; function readArray(offset:uint = 0, length:uint = 0):Array; function readString():String; function hasNext():Boolean; function isReady():Boolean; function reset():void; } } As we've already mentioned, our IReader interface extends IEventDispatcher, which means any implementing class must implement all of the IEventDispatcher methods in addition to the methods required by IReader. The IEventDispatcher interface requires the following methods: addEventListener(), removeEventListener(), dispatchEvent(), hasEventListener(), and willTrigger(). As you'll see in the next section, the simplest way to implement the interface in most cases is simply to extend EvenTDispatcher. The IReader interface also requires a handful of methods. The read(), readArray(), and readString() methods each provide a mechanism for accessing an element or elements of text. The actual implementations will differ in concrete classes, but the idea remains the same: read() returns the next element in much the same way an iterator returns the next element. The readArray() method returns an array of elements. The readString() method returns the original value. The isReady() method returns a Boolean indicating whether or not the reader is ready for reading. The hasNext() method returns a Boolean indicating whether or not there are additional elements. The reset() method resets the reader to the first element. Defining an Abstract Reader ClassAs we've already seen, all implementing classes of IReader must implement quite a few methodsboth those from IEventDispatcher and from IReader. In many of the reader classes, the implemented methods look very similar. For that reason, we can simplify those classes by first defining an abstract class. In this case, we'll define com.peachpit.aas3wdp.decoratorexample.io.AbstractReader as an abstract class implementing the IReader interface. Because the IReader interface extends IEventDispatcher, you can either implement all the required methods or extend a class that already implements those methods. In this case, Reader extends Eventdispatcher, a class that is part of the Flash Player API. package com.peachpit.aas3wdp.decoratorexample.io { import flash.events.Event; import flash.events.EventDispatcher; public class AbstractReader extends EventDispatcher implements IReader { protected var index:uint = 0; public function Reader() { } public function hasNext():Boolean { return false; } public function reset():void { index = 0; } public function isReady():Boolean { return true; } public function read():String { return null; } public function readArray(offset:uint = 0, length:uint = 0):Array { return null; } public function readString():String { return null; } } } The preceding code is fairly basic. Most of the methods simply return default values. The only actual implementation is the declaration of index and the definition of the reset() method. Defining the Concrete Decorated ClassIn this example, we'll build two concrete decorated types. The first StringReader, is the first concrete decorated class and is the simpler of the two.
The StringReader class accepts one parameter in the constructor. It stores that value in a private property. It then defines the read() and readArray() methods to return one character at a time from that value. Here's a simple example of how you could use the StringReader: var reader:IReader = new StringReader("abcdefg"); while(reader.hasNext()) { trace(reader.read()); } This example writes the characters a, b, c, d, e, f, and g one at a time to the console. Creating the Abstract Decorator ClassNow that we've defined a concrete decorated type, we can look at creating decorators for it. To simplify the decorators, we'll create an abstract decorator called com.peachpit.aas3wdp.decoratorexample.io.AbstractReaderDecorator. The AbstractReaderDecorator class extends AbstractReader just like the StringReader class does (all the decorated and decorator classes must implement the same interface). Note that we need to override several of the methods so that they delegate requests to the decorated instance, _content. package com.peachpit.aas3wdp.decoratorexample.io { import com.peachpit.aas3wdp.decoratorexample.io.IReader; import com.peachpit.aas3wdp.decoratorexample.io.Reader; public class AbstractReaderDecorator extends AbstractReader { private var _content:IReader; // The constructor accepts a parameter of type IReader // that will be the decorated object. public function ReaderDecorator(reader:IReader) { _content = reader; } override public function read():String { return _content.read(); } override public function readArray(offset:uint = 0, length:uint = 0):Array { return _content.readArray(offset, length); } override public function readString():String { return _content.readString(); } } } The abstract decorator class is quite simple. It just delegates requests. Next we'll look at creating concrete decorators. Defining the First Concrete Decorator ClassNext, we define the first of the concrete decorator classes, WordReader. Notice that this class extends the abstract decorator class, AbstractReaderDecorator. package com.peachpit.aas3wdp.decoratorexample.io { import com.peachpit.aas3wdp.decoratorexample.io.IReader; import com.peachpit.aas3wdp.decoratorexample.io.ReaderDecorator; public class WordReader extends ReaderDecorator { private var _words:Array; public function WordReader(reader:IReader) { // Call the superclass constructor, passing the // parameter along so that the _content property is // set. super(reader); // Define a regular expression to find words. var expression:RegExp = /[a-z]+/ig; // Retrieve all the words from the decorated content // by calling readString() and using the match() // method with the regular expression. Note that // readString() is implemented in the abstract // ReaderDecorator class. _words = readString().match(expression); } // Override read() so it returns the next word from the // _words array. override public function read():String { var word:String = _words[_index++]; return word; } // Override readArray() so it returns a new array // containing part of the _words array. override public function readArray(offset:uint = 0, length:uint = 0):Array { return _words.slice(offset, length); } override public function hasNext():Boolean { return _index < _words.length; } } } This decorator allows you to wrap any other object that implements the IReader interface, and it changes the functionality while keeping the same interface. For example, the following code illustrates this operation: var reader:IReader = new StringReader("one two three four"); // First display each character one at a time. while(reader.hasNext()) { trace(reader.read()); } reader = new WordReader(reader); // Next display one word at a time. while(reader.hasNext()) { trace(reader.read()); } Testing the DecoratorNext, we'll define a main class to test the example. package { import flash.display.Sprite; import com.peachpit.aas3wdp.decoratorexample.io.IReader; import com.peachpit.aas3wdp.decoratorexample.io.StringReader; import com.peachpit.aas3wdp.decoratorexample.io.WordReader; public class ReaderDecoratorExample extends Sprite { public function ReaderDecoratorExample() { var stringReader:StringReader = new StringReader("Lorem ipsum\ndolor sit amet"); var wordReader:WordReader = new WordReader(stringReader); traceReader(stringReader); traceReader(wordReader); } public function traceReader(reader:IReader):void { while(reader.hasNext()) { trace(reader.read()); } } } } Debug the application, and you'll see that the first call to traceReader() displays each of the characters of the string from the StringReader object. The second call to traceReader() displays each word using the WordReader object. Because traceReader() is defined to accept an IReader parameter, either StringReader (decorated) or WordReader (decorator) will work. Each object implements the same interface, but they have different behaviors. Defining an Additional Concrete Decorator ClassOne of the advantages of the Decorator pattern is that you can create many decorators. Because each decorator can decorate any other decorator, there is no real limit to how many decorators you can use. To illustrate this truth, we'll next add a new decorator class called com.peachpit.aas3wdp.decoratorexample.io.LineReader. As the name implies, LineReader reads the content one line at a time. package com.peachpit.aas3wdp.decoratorexample.io { import com.peachpit.aas3wdp.decoratorexample.io.IReader; import com.peachpit.aas3wdp.decoratorexample.io.ReaderDecorator; public class LineReader extends AbstractReaderDecorator { private var _lines:Array; public function LineReader(reader:IReader) { super(reader); var expression:RegExp = /[\n\r\f]/g; _lines = readString().split(expression); } override public function read():String { var line:String = _lines[_index++]; return line; } override public function readArray(offset:uint = 0, length:uint = 0):Array { return _lines.concat(); } override public function hasNext():Boolean { return _index < _lines.length; } } } The LineReader class inherits from AbstractReaderDecorator. It accepts any reader type as a parameter to the constructor, and it uses that parameter as the content which it then parses into an array of lines. With the addition of the LinerReader, we can next verify that it works by making a few edits to the main class as follows: package { import flash.display.Sprite; import com.peachpit.aas3wdp.decoratorexample.io.IReader; import com.peachpit.aas3wdp.decoratorexample.io.StringReader; import com.peachpit.aas3wdp.decoratorexample.io.WordReader; import com.peachpit.aas3wdp.decoratorexample.io.LineReader; public class ReaderDecoratorExample extends Sprite { public function DecoratorExample() { var stringReader:StringReader = new StringReader("Lorem ipsum\ndolor sit amet"); var wordReader:WordReader = new WordReader(stringReader); var lineReader:LineReader = new LineReader(stringReader); traceReader(stringReader); traceReader(wordReader); traceReader(lineReader); } public function traceReader(reader:IReader):void { while(reader.hasNext()) { trace(reader.read()); } } } } When you debug the application, you'll see that this time the third call to traceReader() outputs each line of text. Because the original text has a newline character (\n), the output displays on two lines. Defining a New Decorated TypeNow that we have a decorated type in place, we've already seen that we can add as many decorators as we want. However, we can also add more decorated types. All that's required is that the new decorated type implements the same interface as all the existing decorated types and decorators. To illustrate this point, we'll next define a new decorated type, com.peachpit.aas3wdp.decoratorexample.io.FileReader. The FileReader class allows you to load the contents of a file. By default, it reads one character at a time similar to the StringReader. package com.peachpit.aas3wdp.decoratorexample.io { import flash.net.URLLoader; import flash.net.URLRequest; import flash.events.Event; public class FileReader extends AbstractReader { private var _content:String; private var _file:URLLoader; private var _canRead:Boolean = false; public function FileReader(file:String) { // Use a URLLoader object to load the text from a // file specified by the parameter. _file = new URLLoader(); var request:URLRequest = new URLRequest(file); _file.load(request); // Call onData() when the content loads. _file.addEventListener(Event.COMPLETE, onData); } private function onData(event:Event):void { // Set the content to the data loaded from the file. _content = String(_file.data); // The object is read for reading. _canRead = true; // Dispatch an event notifying listeners that the // object is ready. dispatchEvent(new Event(Event.COMPLETE)); } override public function isReady():Boolean { return _canRead; } override public function read():String { return _content.charAt(_index++); } override public function readString():String { return _content; } override public function hasNext():Boolean { return _index < _content.length; } } } This class loads text from a URL. Because the data loads asynchronously, the isReady() method returns false until the data has been loaded. Otherwise, it functions very similarly to the StringReader class. With the addition of this new decorated type, we can test it by redefining the main class to use a FileReader instance instead of a StringReader instance. Because FileReader is asynchronous, we'll listen for a COMPLETE event, decorate the object with a WordReader object, and then call traceReader(). Note For this example to work, you'll need a text file called data.txt. You can save a text file in the same directory to which you deploy the .swf from this example. In the text file, you can add text such as the string passed to the StringReader constructor in the earlier example. package { import flash.display.Sprite; import com.peachpit.aas3wdp.decoratorexample.io.IReader; import com.peachpit.aas3wdp.decoratorexample.io.FileReader; import com.peachpit.aas3wdp.decoratorexample.io.WordReader; import flash.events.Event; public class ReaderDecoratorExample extends Sprite { public function ReaderDecoratorExample() { var fileReader:FileReader = new FileReader("data.txt"); fileReader.addEventListener(Event.COMPLETE, onFile); } private function onFile(event:Event):void { var wordReader:WordReader = new WordReader(FileReader(event.target)); traceReader(wordReader); } public function traceReader(reader:IReader):void { while(reader.hasNext()) { trace(reader.read()); } } } } This code illustrates that the same decorator we used in conjunction with a StringReader object can be used with a FileReader object as well. Decorating DecoratorsTo illustrate that decorators can potentially decorate decorators, we'll define a new decorator class called com.peachpit.aas3wdp.decoratorexample.io.SortedReader. SortedReader allows you to access the contents of a decorated reader in a sorted order. package com.peachpit.aas3wdp.decoratorexample.io { public class SortedReader extends AbstractReaderDecorator { private var _content:Array; public function SortedReader(reader:IReader) { super(reader); // Read all the content from the decorated reader as // an array. Then sort that content. _content = reader.readArray().concat(); _content.sort(); } override public function read():String { return _content[_index++]; } override public function readArray(offset:uint = 0, length:uint = 0):Array { var data:Array = new Array(); for(var i:uint = offset; i < length; i++) { data.push(_content[i]); } return data; } override public function hasNext():Boolean { return _index < _content.length; } } } We can see how this new decorator works by editing the main class as follows: package { import flash.display.Sprite; import com.peachpit.aas3wdp.decoratorexample.io.IReader; import com.peachpit.aas3wdp.decoratorexample.io.FileReader; import com.peachpit.aas3wdp.decoratorexample.io.WordReader; import com.peachpit.aas3wdp.decoratorexample.io.SortedReader; import flash.events.Event; public class ReaderDecoratorExample extends Sprite { public function DecoratorExample2() { var fileReader:FileReader = new FileReader("data.txt"); fileReader.addEventListener(Event.COMPLETE, onFile); } private function onFile(event:Event):void { var wordReader:WordReader = new WordReader(FileReader(event.target)); var sortedReader:SortedReader = new SortedReader(wordReader); traceReader(sortedReader); } public function traceReader(reader:IReader):void { while(reader.hasNext()) { trace(reader.read()); } } } } When you test the application this time, all the words are traced in alphabetical order. The SortedReader decorator can decorate any object that implements the IReader interface. That means you can use a SortedReader instance to decorate a WordReader instance as was done in this example, but you can also use it to decorate a LineReader or any other object that implements IReader. |