Flash Gordon
05-25-2009, 01:11 AM
This isn't a question but just an example of a pattern I've been finding really helpful lately and it's the awesome decorator pattern. It's a pattern I've neglected a lot until as of late. It's seems to have become my best friend now. I use it to hide asynchronous behavior and to add other "common" behavior.
So why am I writing this? Well, I think using the decorator pattern on Duck, Geese, and Coffee rather sucks, don't you? (Sorry Head First, I love you guys but thought you could use some help). I've never made a Duck in Flash and never will. However, I make sliders, programmatic skins (which I have decorators for resizing behavior), data controllers, centering scripts all of which use decorators.
I've written an example (attached) of a very common use that I have for decorators and that I think you all should have as well. It comes in the form of a slider control. Sliders can be used for a bunch of things: your scrollbar slider to the right there >>>, a volume slider, a panning slider, etc. The problem I was having was for a volume slider, the typical set up is the bottom of the slider is the min value and the top is the max value. Well, this worked great for me for volume controls but for a slider that controlled scrolling, it was completely backwards. I could either invert the values everywhere I needed it or add a property of ALL of my sliders to toggle an inversion for me.....or....I could add write 1 decorator that would invert all the values for me. This puts my logic all in 1 location rather than several and leaves my original class closed from modification. So here's what I did and it's pretty simple:
I first made an AbstractSliderDecorator so that I could make many decorators for my sliders (and I did) which would make it easier to make the decorators. All i did was delegate all of my property calls to a slider passed into the constructor.
package com.fg.controls
{
import flash.events.EventDispatcher;
import com.fg.events.SliderEvent;
public class AbstractSliderDecorator extends EventDispatcher implements ISlider
{
private var slider:ISlider;
public function get minValue():Number { return slider.minValue; }
public function set minValue(value:Number):void { slider.minValue = value; }
public function get maxValue():Number { return slider.maxValue; }
public function set maxValue(value:Number):void { slider.maxValue = value; }
public function get value():Number { return slider.value; }
public function set value(value:Number):void { slider.value = value; }
public function AbstractSliderDecorator(slider:ISlider):void
{
this.slider = slider;
this.slider.addEventListener(SliderEvent.SLIDER_CH ANGE, bubbleHandler, false, 0, true);
}
private function bubbleHandler(e:SliderEvent):void
{
dispatchEvent( e );
}
}
}
So then I made my InvertedSliderDecorator which was pretty easy now. All I didn't was overrode the value properties to give the appearance the value is inverted. Which I thought was pretty slick.
package com.fg.controls
{
import flash.events.EventDispatcher;
import com.fg.events.SliderEvent;
public class InvertedSliderDecorator extends AbstractSliderDecorator
{
private var slider:ISlider;
override public function get value():Number { return invertValue(slider.value); }
override public function set value(value:Number):void { slider.value = invertValue(value); }
public function InvertedSliderDecorator(slider:ISlider):void
{
super(slider);
this.slider = slider;
addEventListener(SliderEvent.SLIDER_CHANGE, sliderChangeHandler, false, 1, true);
}
private function invertValue(value:Number):Number
{
var difference:Number = value - slider.minValue;
return slider.maxValue - difference;
}
private function sliderChangeHandler(e:SliderEvent):void
{
e.value = invertValue(e.value);
}
}
}
And lastly here is my slider class. This class is a sample of my slider. It SHOULD be abstracted out and I hinted at where that should be by exposing protected methods. The only difference between a HorizontalSlilder and VeritcalSlider could be the commits to the value property.
package com.fg.controls
{
import flash.display.DisplayObject;
import flash.display.Shape;
import flash.display.Sprite;
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.MouseEvent;
import flash.geom.Rectangle;
import com.fg.events.SliderEvent;
public class VerticalSlider extends EventDispatcher implements ISlider
{
private var dummy:Shape;
private function get limitsChange():Number { return maxValue - minValue; }
private var _handle:Sprite;
public function get handle():Sprite { return _handle; }
private var _track:DisplayObject;
public function get track():DisplayObject { return _track; }
private var _bounds:Rectangle;
public function get bounds():Rectangle { return _bounds; }
public function set bounds(value:Rectangle):void
{
if (_bounds && _bounds.equals(value)) return;
_bounds = value;
}
private var _minValue:Number = 0;
public function get minValue():Number { return _minValue; }
public function set minValue(value:Number):void
{
_minValue = value;
}
private var _maxValue:Number = 1;
public function get maxValue():Number { return _maxValue; }
public function set maxValue(value:Number):void
{
_maxValue = value;
}
private var _value:Number;
public function get value():Number { return _value; }
public function set value(value:Number):void
{
if (_value == value) return;
_value = value;
commitValueProperty();
dispatchEvent( new SliderEvent(SliderEvent.SLIDER_CHANGE, this.value) );
}
public function VerticalSlider()
{
super();
dummy = new Shape();
}
public function initAssets(handle:Sprite, track:DisplayObject):void
{
_handle = handle;
_track = track;
handle.buttonMode = true;
handle.addEventListener(MouseEvent.MOUSE_DOWN, handleMouseDownHandler, false, 0, true);
handle.addEventListener(MouseEvent.MOUSE_UP, handleMouseUpHandler, false, 0, true);
bounds = track.getBounds(handle.parent);
bounds.height -= handle.height;
bounds.width = 0;
bounds.x = handle.x;
}
protected function commitValueProperty():void
{
var percent:Number = 1-(value - minValue) / limitsChange
var location:Number = percent * bounds.height + bounds.y;
handle.y = location;
}
protected function calculateValue():void
{
var percent:Number = 1-(handle.y - bounds.y) / bounds.height;
var calculatedValue = percent * limitsChange + minValue;
this.value = calculatedValue;
}
protected function handlePressed():void
{
handle.startDrag(false, bounds);
dummy.addEventListener(Event.ENTER_FRAME, dummyEnterFrameHandler, false, 0, true);
}
protected function handleReleased():void
{
handle.stopDrag();
dummy.removeEventListener(Event.ENTER_FRAME, dummyEnterFrameHandler);
}
private function dummyEnterFrameHandler(e:Event):void
{
calculateValue();
}
private function handleMouseDownHandler(e:MouseEvent):void
{
handlePressed();
handle.stage.addEventListener(MouseEvent.MOUSE_UP, handleMouseUpHandler, false, 0, true);
}
private function handleMouseUpHandler(e:MouseEvent):void
{
handleReleased();
handle.stage.removeEventListener(MouseEvent.MOUSE_ UP, handleMouseUpHandler);
}
}
}
package com.fg.controls
{
import flash.events.IEventDispatcher;
public interface ISlider extends IEventDispatcher
{
function get minValue():Number;
function set minValue(value:Number):void;
function get maxValue():Number;
function set maxValue(value:Number):void;
function get value():Number;
function set value(value:Number):void;
}
}
If you look at the attached fla/swf you'll see two sliders: one has the decorator on it and the other does not. I could now use my decorator on an HorizontalSlider as well as my VerticalSlider (or even an crazy LoopDeLoopSlider). Same code reused and easily maintainable. And there are many other decorators that could be applied to a slider such as a EvenValueSliderDecorator for instance.
THANKS DECORATORS!
So why am I writing this? Well, I think using the decorator pattern on Duck, Geese, and Coffee rather sucks, don't you? (Sorry Head First, I love you guys but thought you could use some help). I've never made a Duck in Flash and never will. However, I make sliders, programmatic skins (which I have decorators for resizing behavior), data controllers, centering scripts all of which use decorators.
I've written an example (attached) of a very common use that I have for decorators and that I think you all should have as well. It comes in the form of a slider control. Sliders can be used for a bunch of things: your scrollbar slider to the right there >>>, a volume slider, a panning slider, etc. The problem I was having was for a volume slider, the typical set up is the bottom of the slider is the min value and the top is the max value. Well, this worked great for me for volume controls but for a slider that controlled scrolling, it was completely backwards. I could either invert the values everywhere I needed it or add a property of ALL of my sliders to toggle an inversion for me.....or....I could add write 1 decorator that would invert all the values for me. This puts my logic all in 1 location rather than several and leaves my original class closed from modification. So here's what I did and it's pretty simple:
I first made an AbstractSliderDecorator so that I could make many decorators for my sliders (and I did) which would make it easier to make the decorators. All i did was delegate all of my property calls to a slider passed into the constructor.
package com.fg.controls
{
import flash.events.EventDispatcher;
import com.fg.events.SliderEvent;
public class AbstractSliderDecorator extends EventDispatcher implements ISlider
{
private var slider:ISlider;
public function get minValue():Number { return slider.minValue; }
public function set minValue(value:Number):void { slider.minValue = value; }
public function get maxValue():Number { return slider.maxValue; }
public function set maxValue(value:Number):void { slider.maxValue = value; }
public function get value():Number { return slider.value; }
public function set value(value:Number):void { slider.value = value; }
public function AbstractSliderDecorator(slider:ISlider):void
{
this.slider = slider;
this.slider.addEventListener(SliderEvent.SLIDER_CH ANGE, bubbleHandler, false, 0, true);
}
private function bubbleHandler(e:SliderEvent):void
{
dispatchEvent( e );
}
}
}
So then I made my InvertedSliderDecorator which was pretty easy now. All I didn't was overrode the value properties to give the appearance the value is inverted. Which I thought was pretty slick.
package com.fg.controls
{
import flash.events.EventDispatcher;
import com.fg.events.SliderEvent;
public class InvertedSliderDecorator extends AbstractSliderDecorator
{
private var slider:ISlider;
override public function get value():Number { return invertValue(slider.value); }
override public function set value(value:Number):void { slider.value = invertValue(value); }
public function InvertedSliderDecorator(slider:ISlider):void
{
super(slider);
this.slider = slider;
addEventListener(SliderEvent.SLIDER_CHANGE, sliderChangeHandler, false, 1, true);
}
private function invertValue(value:Number):Number
{
var difference:Number = value - slider.minValue;
return slider.maxValue - difference;
}
private function sliderChangeHandler(e:SliderEvent):void
{
e.value = invertValue(e.value);
}
}
}
And lastly here is my slider class. This class is a sample of my slider. It SHOULD be abstracted out and I hinted at where that should be by exposing protected methods. The only difference between a HorizontalSlilder and VeritcalSlider could be the commits to the value property.
package com.fg.controls
{
import flash.display.DisplayObject;
import flash.display.Shape;
import flash.display.Sprite;
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.MouseEvent;
import flash.geom.Rectangle;
import com.fg.events.SliderEvent;
public class VerticalSlider extends EventDispatcher implements ISlider
{
private var dummy:Shape;
private function get limitsChange():Number { return maxValue - minValue; }
private var _handle:Sprite;
public function get handle():Sprite { return _handle; }
private var _track:DisplayObject;
public function get track():DisplayObject { return _track; }
private var _bounds:Rectangle;
public function get bounds():Rectangle { return _bounds; }
public function set bounds(value:Rectangle):void
{
if (_bounds && _bounds.equals(value)) return;
_bounds = value;
}
private var _minValue:Number = 0;
public function get minValue():Number { return _minValue; }
public function set minValue(value:Number):void
{
_minValue = value;
}
private var _maxValue:Number = 1;
public function get maxValue():Number { return _maxValue; }
public function set maxValue(value:Number):void
{
_maxValue = value;
}
private var _value:Number;
public function get value():Number { return _value; }
public function set value(value:Number):void
{
if (_value == value) return;
_value = value;
commitValueProperty();
dispatchEvent( new SliderEvent(SliderEvent.SLIDER_CHANGE, this.value) );
}
public function VerticalSlider()
{
super();
dummy = new Shape();
}
public function initAssets(handle:Sprite, track:DisplayObject):void
{
_handle = handle;
_track = track;
handle.buttonMode = true;
handle.addEventListener(MouseEvent.MOUSE_DOWN, handleMouseDownHandler, false, 0, true);
handle.addEventListener(MouseEvent.MOUSE_UP, handleMouseUpHandler, false, 0, true);
bounds = track.getBounds(handle.parent);
bounds.height -= handle.height;
bounds.width = 0;
bounds.x = handle.x;
}
protected function commitValueProperty():void
{
var percent:Number = 1-(value - minValue) / limitsChange
var location:Number = percent * bounds.height + bounds.y;
handle.y = location;
}
protected function calculateValue():void
{
var percent:Number = 1-(handle.y - bounds.y) / bounds.height;
var calculatedValue = percent * limitsChange + minValue;
this.value = calculatedValue;
}
protected function handlePressed():void
{
handle.startDrag(false, bounds);
dummy.addEventListener(Event.ENTER_FRAME, dummyEnterFrameHandler, false, 0, true);
}
protected function handleReleased():void
{
handle.stopDrag();
dummy.removeEventListener(Event.ENTER_FRAME, dummyEnterFrameHandler);
}
private function dummyEnterFrameHandler(e:Event):void
{
calculateValue();
}
private function handleMouseDownHandler(e:MouseEvent):void
{
handlePressed();
handle.stage.addEventListener(MouseEvent.MOUSE_UP, handleMouseUpHandler, false, 0, true);
}
private function handleMouseUpHandler(e:MouseEvent):void
{
handleReleased();
handle.stage.removeEventListener(MouseEvent.MOUSE_ UP, handleMouseUpHandler);
}
}
}
package com.fg.controls
{
import flash.events.IEventDispatcher;
public interface ISlider extends IEventDispatcher
{
function get minValue():Number;
function set minValue(value:Number):void;
function get maxValue():Number;
function set maxValue(value:Number):void;
function get value():Number;
function set value(value:Number):void;
}
}
If you look at the attached fla/swf you'll see two sliders: one has the decorator on it and the other does not. I could now use my decorator on an HorizontalSlider as well as my VerticalSlider (or even an crazy LoopDeLoopSlider). Same code reused and easily maintainable. And there are many other decorators that could be applied to a slider such as a EvenValueSliderDecorator for instance.
THANKS DECORATORS!