Building Visual and Commutative Decorators


The preceding example gave you a chance to use the Decorator pattern in a fairly simple example. For the most part, the decorators in the preceding example were not chainable; that is, you couldn't meaningfully composite a WordReader within a LineReader object. Although the SortedReader object does enable you to chain decorators, you must apply the SortedReader as a decorator last in the chain, which means that the decorators are not commutative. Decorators are commutative if the following code yields effectively the same behavior, where A and B are both decorators and decorated is the decorated object:

var a:A = new A(decorated); var b:B = new B(a); b.method();     var b:B = new B(decorated); var a:A = new A(b); a.method();


You can see that SortedReader is not commutative because it matters what order it is applied in the chain. For example, if you apply a WordReader as a decorator to a SortedReader object, you will get different results than if you apply a SortedReader to a WordReader. This doesn't make the preceding example bad or impractical. It simply illustrates that the Decorator pattern is flexible enough to allow for chainable and non-chainable as well as commutative and non-commutative decorators.

In this next example, you'll have a chance to see how to build visual decorators that are chainable and commutative. This example decorates shapes (circles and rectangles) by making them draggable and by adding bevels to them.

Defining the Common Interface

Define com.peachpit.aas3wdp.shapes.AbstractBasicShape as an abstract class that serves as the common interface. In this example, we're using an abstract class with no implementation rather than an interface because we want all the shapes (decorated and decorator) to be a subtype of Sprite so that all the shapes can rely on the Sprite interface. There is no built-in interface for Sprite or any sort of display object. Normally, it is best to use an actual interface construct. However, in this case, we're going to use an abstract class in place of an interface for two reasons:

  1. The required interface is really long. Although this wouldn't excuse not using a proper interface in a typical scenario, we want to simplify things here rather than occupying several printed pages with the interface code.

  2. All the concrete classes would have to extend Sprite in order to inherit the critical display object behavior.

For our purposes, we want to create a unique type that implements the entire Sprite interface. Therefore, the simplest thing to do is to create an abstract class that merely inherits from Sprite. It doesn't require any further implementation. Again, we're making a minor exception to our rule that all abstract classes must have some sort of implementation for this example because of the special case it presents.

package com.peachpit.aas3wdp.shapes {          import flash.display.Sprite;          public class AbstractBasicShape extends Sprite {                 public function AbstractBasicShape() {       }    } }


Defining Concrete Decorated Classes

Next, we're going to create a few concrete decorated types. First we'll create a Circle class. Define com.peachpit.aas3wdp.shapes.Circle as a class that draws a circle.

package com.peachpit.aas3wdp.shapes {          import com.peachpit.aas3wdp.shapes.AbstractBasicShape;    import flash.display.Sprite;       public class Circle extends AbstractBasicShape {       public function Circle(radius:Number) {          var shape:Sprite = new Sprite();          addChild(shape);          shape.graphics.lineStyle(0, 0, 0);          shape.graphics.beginFill(0xFFFFFF);          shape.graphics.drawCircle(radius, radius, radius);          shape.graphics.endFill();       }    } }


The second decorated type is the Rectangle class. Define com.peachpit.aas3wdp.decoratorexample.shapes.Rectangle as a class that draws a rectangle.

package com.peachpit.aas3wdp.shapes {    import com.peachpit.aas3wdp.shapes.AbstractBasicShape;    import flash.display.Sprite;    public class Rectangle extends AbstractBasicShape {       public function Rectangle(shapeWidth:Number, shapeHeight:Number,            center:Boolean = false) {          var shape:Sprite = new Sprite();          addChild(shape);          shape.graphics.lineStyle(0, 0, 0);          shape.graphics.beginFill(0xFFFFFF);          shape.graphics.drawRect(center ? -shapeWidth / 2 : 0, center ? -shapeHeight             / 2 : 0, shapeWidth, shapeHeight);          shape.graphics.endFill();       }    } }


Both of these decorated types are fairly straightforward. They are basic shape types we can decorate with the decorators we're about to create. Apart from the fact that each of these types implement the same interface (which they inherit from AbstractBasicShape), there is nothing about these classes that is unique to the Decorator pattern.

Defining Decorator Classes

Now we can create the decorator classes. Define com.peachpit.aas3wdp.decoratorexample.shapes.DraggableShape as a decorator class that wraps a AbstractBasicShape object and makes it draggable.

package com.peachpit.aas3wdp.shapes {    import com.peachpit.aas3wdp.shapes.AbstractBasicShape;    import flash.events.MouseEvent;    import flash.display.Sprite;    public class DraggableShape extends AbstractBasicShape {       private var _decorated:AbstractBasicShape;       public function DraggableShape(AbstractBasicShape:AbstractBasicShape) {          _decorated = AbstractBasicShape;          addChild(_decorated);          addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);          addEventListener(MouseEvent.MOUSE_UP, onMouseUp);       }       private function onMouseDown(event:MouseEvent):void {          startDrag();       }               private function onMouseUp(event:MouseEvent):void {          stopDrag();       }    } }


The constructor for this class accepts an instance of a concrete basic shape and then acts as a container for that object. By acting as a container, the drag behaviors added to the DraggableShape object make the child (the decorated object) draggable by proxy.

Note

The DraggableShape implementation is purposefully simple. It does not take into account the possibility that the user could move the mouse outside of the object while dragging it. In such a case, the user could inadvertently cause the object to continue to follow the mouse even after releasing the mouse button. We've opted not to show the code to solve that issue because we want to keep this example as focused as possible in demonstrating the Decorator pattern.


Next we'll create a second decorator. Our next decorator class is BevelShape, which adds a bevel to the object. Define com.peachpit.aas3wdp.decoratorexample.shapes.BevelShape as a class that wraps a AbstractBasicShape object and applies a bevel filter.

package com.peachpit.aas3wdp.shapes {    import flash.filters.BevelFilter;    public class BevelShape extends AbstractBasicShape {         public function BevelShape(shape:AbstractBasicShape) {          var filters:Array = shape.filters;          filters.push(new BevelFilter());          shape.filters = filters;          addChild(shape);        }    } }


Now we can test that the decorators are commutable and chainable. Define a main class that adds and decorates shapes.

package {    import flash.display.Sprite;    import flash.display.Stage;    import flash.display.StageScaleMode;    import flash.display.StageAlign;    import com.peachpit.aas3wdp.shapes.AbstractBasicShape;    import com.peachpit.aas3wdp.shapes.Circle;    import com.peachpit.aas3wdp.shapes.Rectangle;    import com.peachpit.aas3wdp.shapes.DraggableShape;    import com.peachpit.aas3wdp.shapes.BevelShape;          public class ShapeDecoratorExample extends Sprite {        public function DecoratorExample() {            stage.scaleMode = StageScaleMode.NO_SCALE;          stage.align = StageAlign.TOP_LEFT;          var shape:AbstractBasicShape;                    // Create a rectangle. Make it draggable first, then          // add a bevel.          shape = new Rectangle(200, 200);          shape = new DraggableShape(shape);          shape = new BevelShape(shape);          shape.x = 200;          shape.y = 200;          addChild(shape);          // Create a circle. Apply the bevel, then make it          // draggable.          shape = new Circle(100);          shape = new BevelShape(shape);          shape = new DraggableShape(shape);          addChild(shape);       }           } }


Notice that, in the preceding example, it makes no difference if you apply the bevel first or make the object draggable first. Both ways work equally well.

Adding Non-Commutative Decorators

Next, we'll add two non-commutative decorators to see the contrast with the first two decorators. The first non-commutative decorator is ColorableShape. Define com.peachpit.aas3wdp.

shapes.ColorableShape as follows.

package com.peachpit.aas3wdp.shapes {    import flash.geom.ColorTransform;    public class ColorableShape extends AbstractBasicShape {       public function ColorableShape(shape:AbstractBasicShape, red:Number,          green:Number, blue:Number) {          shape.transform.colorTransform = new ColorTransform(red, green, blue);          addChild(shape);       }           } }


The second non-commutative decorator is ResizableShape, which adds a resize handler to the shape. The resize handler allows the user to click and drag to change the width and height of the shape. Define com.peachpit.aas3wdp.shapes.ResizableShape as follows.

package com.peachpit.aas3wdp.shapes {    import com.peachpit.aas3wdp.shapes.AbstractBasicShape;    import com.peachpit.aas3wdp.shapes.ColorableShape;    import flash.display.Sprite;    import flash.events.MouseEvent;    import flash.events.Event;    public class ResizableShape extends AbstractBasicShape {          private var _isResizing:Boolean;       private var _resizer:AbstractBasicShape;       private var _decorated:AbstractBasicShape;       // Override the width and height setters so when you       // attempt to set the width and height you set the width       // and height of the decorated object and move the resizer       // handle appropriately.       override public function set width(value:Number):void {          _decorated.width = value;          _resizer.x = value;       }       override public function set height(value:Number):void {          _decorated.height = value;          _resizer.y = value;       }       public function ResizableShape(AbstractBasicShape:AbstractBasicShape) {          _decorated = AbstractBasicShape;          addChild(_decorated);          // Create a new rectangle that is centered to serve          // as the resize handle. Use ColorableShape to make          // the rectangle gray. Then move the rectangle to          // the lower-right corner of the decorated shape.          _resizer = new Rectangle(10, 10, true);          _resizer = new ColorableShape(_resizer, .8, .8, .8);          _resizer.x = _decorated.width;          _resizer.y = _decorated.height;          addChild(_resizer);          _resizer.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);          _resizer.addEventListener(MouseEvent.MOUSE_UP, onMouseUp);       }        // When the user clicks on the resize handle, make it        // draggable, start listening for enterFrame events.        private function onMouseDown(event:MouseEvent):void {           addEventListener(Event.ENTER_FRAME, onEnterFrame);           _resizer.startDrag(true);           event.stopImmediatePropagation();        }                  // When the user releases the mouse click, stop making        // the resize handle draggable, and stop listening for        // enterFrame events.        private function onMouseUp(event:MouseEvent):void {           _resizer.stopDrag();           removeEventListener(Event.ENTER_FRAME, onEnterFrame);        }        private function onEnterFrame(event:Event):void {          // Don't allow the user to move the resize          // handle in negative directions.          if(_resizer.x < 0) {             _resizer.x = 0;          }          if(_resizer.y < 0) {             _resizer.y = 0;          }          // Change the width and height of the decorated          // object to correspond to the resize handle x          // and y coordinate values.          _decorated.width = _resizer.x;          _decorated.height = _resizer.y;       }    } }


The ResizableShape object wraps decorated objects much like the other decorators we've seen so far in this example. However, it also draws a resize handle within itself. As the user moves the resize handle, the ResizableShape instance adjusts the width and height of the decorated object.

Now that we've created two additional decorators, let's modify the main class so that it uses the two new decorators:

package {           import flash.display.Sprite;    import flash.display.Stage;    import flash.display.StageScaleMode;    import flash.display.StageAlign;    import com.peachpit.aas3wdp.shapes.AbstractBasicShape;    import com.peachpit.aas3wdp.shapes.Circle;    import com.peachpit.aas3wdp.shapes.Rectangle;    import com.peachpit.aas3wdp.shapes.DraggableShape;    import com.peachpit.aas3wdp.shapes.BevelShape;    import com.peachpit.aas3wdp.shapes.ColorableShape;    import com.peachpit.aas3wdp.shapes.ResizableShape;    public class ShapeDecoratorExample extends Sprite {       public function DecoratorExample() {          stage.scaleMode = StageScaleMode.NO_SCALE;          stage.align = StageAlign.TOP_LEFT;          var shape:AbstractBasicShape;          shape = new Rectangle(200, 200);          shape = new DraggableShape(shape);          shape = new BevelShape(shape);          shape = new ColorableShape(shape, 0, 0, 0);          shape.x = 200;          shape.y = 200;          addChild(shape);          shape = new Circle(100);          shape = new BevelShape(shape);          shape = new DraggableShape(shape);          shape = new ResizableShape(shape);          addChild(shape);               }    } }


When you test this version of the application, you'll notice that although the rectangle is blackas you'd expect because of the ColorableShape decoratorit no longer displays the bevel because the color transform was applied after the bevel. Secondly, you'll notice that as you drag the circle, the resize handle does not move with it. That is because the ResizableShape decorator was applied after the DraggableShape decorator.

Next, we can change the order in which the new decorators are applied to illustrate that the order affects the behavior:

package {      import flash.display.Sprite;    import flash.display.Stage;    import flash.display.StageScaleMode;    import flash.display.StageAlign;    import com.peachpit.aas3wdp.shapes.AbstractBasicShape;    import com.peachpit.aas3wdp.shapes.Circle;    import com.peachpit.aas3wdp.shapes.Rectangle;    import com.peachpit.aas3wdp.shapes.DraggableShape;    import com.peachpit.aas3wdp.shapes.BevelShape;    import com.peachpit.aas3wdp.shapes.ColorableShape;    import com.peachpit.aas3wdp.shapes.ResizableShape;           public class DecoratorExample extends Sprite {       public function DecoratorExample() {                       stage.scaleMode = StageScaleMode.NO_SCALE;          stage.align = StageAlign.TOP_LEFT;                     var shape:AbstractBasicShape;          shape = new Rectangle(200, 200);          shape = new DraggableShape(shape);          shape = new ColorableShape(shape, 0, 0, 0);          shape = new BevelShape(shape);          shape.x = 200;          shape.y = 200;          addChild(shape);          shape = new Circle(100);          shape = new BevelShape(shape);          shape = new ResizableShape(shape);          shape = new DraggableShape(shape);          addChild(shape);       }    }


This time, when you test the application, you'll see that the bevel is preserved and that the resize handle moves with the shape. This is because the decorators have been applied in the correct order. The ColorableShape and ResizableShape decorators are perfectly valid, but they are non-commutative.




Advanced ActionScript 3 with Design Patterns
Advanced ActionScript 3 with Design Patterns
ISBN: 0321426568
EAN: 2147483647
Year: 2004
Pages: 132

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net