overview

learn what a design pattern is

use two design patterns:

  • Observer
  • Singleton
  • example: a logging utility to output program status messages

    what is a design pattern?

    design pattern is a widely accepted solution to a recurring design problem in OOP

    a design pattern describes how to structure classes to meet a given requirement

    provides a general blueprint to follow when implementing part of a program

    does not describe how to structure the entire application

    does not describe specific algorithms

    focuses on relationships between classes

    made popular by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (authors of Design Patterns: Elements Of Reusable Object-Oriented Software)

    benefits of design patterns

    learn from community wisdom

    determine implementation faster

    make code more readable to other programmers

    the observer pattern

    simple event-broadcasting system

    changes in the state of one object are reported ("broadcast") to other objects

    object that changes is the subject

    objects that receive updates are the observers

    multiple displays of data

    Observer is perfect for displaying one body of data in multiple ways

    example: a weather reporting application

    WeatherReporter class stores latest weather data

    on-screen, the weather is displayed by two classes:

  • GraphicReport (cloud, sun, rain icons)
  • TextReport (Temperature: 29C, Sunny)
  • when the weather changes, WeatherReporter sends updates to TextReport object and GraphicReport object

    each object deals with the update in its own way

    flexibility, extensibility

    Observer makes an application easy to change

    for example, easy to add new kinds of weather reports

    could add a SoundReport class to provide the spoken weather report

    when the new class is added, the WeatherReporter class does not need to change!

    goal of Observer: allow new functionality to be added/removed at runtime with minimal effort

    responsibilities of the Subject

    the subject class is responsible for:

  • maintaining a list of observer objects
  • providing methods for adding and removing observers
  • sending update notifications to observers
  • the Observable class

    Observable class: base class for all "Subjects"

    Subject classes such as WeatherReporter extend Observable

    main features of Observable:

  • observers, property to hold array of observer objects
  • addObserver(), adds an observer to the observers array
  • removeObserver(), removes an observer from the observers array
  • notifyObservers(), sends update notifications to all observers
  • the observer classes

    all observer classes must implement an update() method

    Subject sends update notifications by invoking update() on observers

    observers implement the Observer interface, which defines the update() method format

    only classes that implement Observer can register for updates from a Subject

    push vs pull

    observers often need information from subject at update time

    for example, need new temperature when a weather report changes

    two ways for observers to get information:

  • pull model: observers invoke methods on Subject requesting information
  • WeatherReporter.getTemperature()

  • push model: Subject passes information to observers as an argument to update()
  • var infoObject.temperature = newTemperature; ... observers[i].update(this, infoObject);

    both push and pull are valid: which to use depends on program requirements

    pull model is less efficient (more method calls on subject)

    push model is less flexible: requires Subject to know the needs of its observers, making observers harder to modify without affecting Subject

    Observer interface source code

    here's the source code listing for Observer

    import util.Observable; interface util.Observer { public function update(o:Observable, infoObj:Object):Void; }

    the o parameter refers to the Subject so that observers can:

  • access methods on the subject (pull model)
  • distinguish one update from another if the observer is registered with multiple subjects
  • infoObj contains information about the update (push model)

    Observable class source

    here's the source code listing for Observable (follows Java's equivalent class)

    import util.Observer; /** * A Java-style Observable class used to represent the "subject" * of the Observer design pattern. Observers must implement the Observer * interface, and register to observe the subject via addObserver(). */ class util.Observable { // A flag indicating whether this object has changed. private var changed:Boolean = false; // A list of observers. private var observers:Array; /** * Constructor function. */ public function Observable () { observers = new Array(); } /** * Adds an observer to the list of observers. * @param o The observer to be added. */ public function addObserver(o:Observer):Boolean { // Can't add a null observer. if (o == null) { return false; } // Don't add an observer more than once. for (var i:Number = 0; i < observers.length; i++) { if (observers[i] == o) { // The observer is already observing, so quit. return false; } } // Put the observer into the list. observers.push(o); return true; } /** * Removes an observer from the list of observers. * * @param o The observer to remove. */ public function removeObserver(o:Observer):Boolean { // Find and remove the observer. var len:Number = observers.length; for (var i:Number = 0; i < len; i++) { if (observers[i] == o) { observers.splice(i, 1); return true; } } return false; } /** * Tell all observers that the subject has changed. * * @param infoObj An object containing arbitrary data * to pass to observers. */ public function notifyObservers(infoObj:Object):Void { // Use a null infoObject if none is supplied. if (infoObj == undefined) { infoObj = null; } // If the object hasn't changed, don't bother notifying observers. if (!changed) { return; } // Make a copy of the observers array. We do this // so that we can be sure the list won't change while // we're processing it. var observersSnapshot:Array = observers.slice(0); // This change has been processed, so unset the "changed" flag. clearChanged(); // Invoke update() on all observers. for (var i:Number = observersSnapshot.length-1; i >= 0; i--) { observersSnapshot[i].update(this, infoObj); } } /** * Removes all observers from the observer list. */ public function clearObservers():Void { observers = new Array(); } /** * Indicates that the subject has changed. */ private function setChanged():Void { changed = true; } /** * Indicates that the subject has either not changed or * has notified its observers of the most recent change. */ private function clearChanged():Void { changed = false; } /** * Checks if the subject has changed. * * @return true if the subject has changed, as determined by setChanged(). */ public function hasChanged():Boolean { return changed; } /** * Returns the number of observers in the observer list. * * @return An integer: the number of observers for this subject. */ public function countObservers():Number { return observers.length; } }

    Logger, an Observer example

    Observer example: an application log that displays system messages on screen

    four classes:

  • Logger, the Subject. stores messsages in the log
  • LogMessage, the info object. contains a single log message
  • OutputPanelView, an observer that displays log messages in the flash output panel
  • TextFieldView, an observer that displays log messages in a text field
  • any class can add messages to the log

    when a message is added, the Logger class notifies its observers of the change

    log message severity

    log messages have 5 levels of importance, represented by 5 integers:

  • FATAL: 0
  • ERROR: 1
  • WARN: 2
  • INFO: 3
  • DEBUG: 4
  • Logger can be set to only report messages of a given level

    e.g., only notify observers of ERROR and FATAL messages:

    log.setLevel(1);

    the LogMessage class

    LogMessage contains two pieces of information:

  • the log message such as "System Failure"
  • the severity of the message, such as 0 (FATAL)
  • instances created by Logger at update time

    code listing for LogMessage

    class logger.LogMessage { private var msg:String; private var level:Number; public function LogMessage (m:String, lev:Number) { setMessage(m); setLevel(lev); } public function setMessage (m:String):Void { msg = m; } public function getMessage ():String { return msg; } public function setLevel (lev:Number):Void { level = lev; } public function getLevel ():Number { return level; } }

    the OutputPanelView class

    OutputPanelView displays messages in flash's output panel

    implements Observer

    import util.Observer; import util.Observable; import logger.Logger; import logger.LogMessage; class logger.OutputPanelView implements Observer { // The log that this object is observing. // Not used in this case, but stored in case: // 1) this observer wants to set the state of the Subject // 2) this observer wants to pull information from the Subject private var log:Logger; public function OutputPanelView (l:Logger) { log = l; } public function update (o:Observable, infoObj:Object):Void { // Cast infoObj to a LogMessage instance for type checking. var logMsg:LogMessage = LogMessage(infoObj); trace(Logger.getLevelDesc(logMsg.getLevel()) + ": " + logMsg.getMessage()); } public function destroy ():Void { log.removeObserver(this); } }

    the TextFieldView class

    TextFieldView displays messages in a text field

    same structure as OutputPanelView, but adds a property and method to manage the text field

    import util.Observer; import util.Observable; import logger.Logger; import logger.LogMessage; class logger.TextFieldView implements Observer { private var log:Logger; private var out:TextField; public function TextFieldView (l:Logger, target:MovieClip, depth:Number, x:Number, y:Number, w:Number, h:Number) { log = l; makeTextField(target, depth, x, y, w, h); } public function update (o:Observable, infoObj:Object):Void { var logMsg:LogMessage = LogMessage(infoObj); out.text += Logger.getLevelDesc(logMsg.getLevel()) + ": " + logMsg.getMessage() + "\n"; out.scroll = out.maxscroll; } public function makeTextField (target:MovieClip, depth:Number, x:Number, y:Number, w:Number, h:Number):Void { target.createTextField("log_txt", depth, x, y, w, h); out = target.log_txt; out.border = true; out.wordWrap = true; } public function destroy ():Void { log.removeObserver(this); out.removeTextField(); } }

    the Logger class

    Logger is the Subject of this Observer implementation

    hence, it extends Observable:

    class logger.Logger extends Observable { }

    Singleton in Logger

    Logger class uses another design pattern to manage instance creation

    Singleton pattern ensures a class has only one instance, and provides a global point of access to it

    perfect for our Logger class (because we only want one log per application)

    the following aspects of Logger are based on Singleton

  • constructor is private (can't be used externally):
  • private function Logger () { // Show "INFO" level messages by default. setLevel(Logger.INFO); }

  • class stores an instance of itself in a class property:
  • private static var log:Logger = null;

  • class provides a class method to retrieve the single instance of itself
  • public static function getLog():Logger { if (log == null) { log = new Logger(); } return log; }

    Logger class, message severity

    log severity levels stored in static properties:

    public static var FATAL:Number = 0; public static var ERROR:Number = 1; public static var WARN:Number = 2; public static var INFO:Number = 3; public static var DEBUG:Number = 4;

    strings to describe severity levels stored in an array:

    public static var levelDescriptions = ["FATAL", "ERROR", "WARN", "INFO", "DEBUG"];

    current severity level stored in logLevel:

    private var logLevel:Number;

    the log's severity level can be set via setLevel():

    public function setLevel(lev:Number):Void { // Make sure the supplied level is an integer. lev = Math.floor(lev); // Set the log level if it's one of the acceptable levels. if (lev >= Logger.FATAL && lev <= Logger.DEBUG) { logLevel = lev; info("Log level set to: " + lev); return; } // If we get this far, the log level isn't valid. warn("Invalid log level specified."); }

    log severity level can be retrieved via getLevel()

    public function getLevel( ):Number { return logLevel; }

    getLevelDesc() returns a human-readable description of the current log level

    public static function getLevelDesc(level:Number):String { return levelDescriptions[level]; }

    state change updates in Logger

    messages are sent to the log according to their severity

    DEBUG messages are sent via Logger.debug()

    INFO messages are sent via Logger.info(), and so on

    for example, this code sends an error message to the log:

    Logger.getLog().error("Something went wrong");

    the most recent message sent to the log is stored in lastMsg:

    private var lastMsg:LogMessage;

    all log message methods have the same structure:

  • perform state change (i.e., set lastMsg)
  • create info object (LogMessage)
  • register state change in subject (via setChanged())
  • broadcast change notification (via notifyObservers())
  • here's the code for the info() method:

    public function info(msg:String):Void { // If the filter level is at least "INFO", broadcast the message to observers. if (logLevel >= Logger.INFO) { lastMsg = new LogMessage(msg, Logger.INFO); setChanged(); notifyObservers(lastMsg); } }

    Logger class code

    here's the code listing for the Logger class:

    import util.Observable; import logger.LogMessage; /** * A general log class. Use getLog() to create an app-wide instance. * Send messages with fatal(), error(), warn(), info(), and debug(). * Add views for the log with addObserver() (views must implement Observer). * * @version 1.0.0 */ class logger.Logger extends Observable { // Static variable. A reference to the log instance (Singleton). private static var log:Logger = null; // The possible log levels for a message. public static var FATAL:Number = 0; public static var ERROR:Number = 1; public static var WARN:Number = 2; public static var INFO:Number = 3; public static var DEBUG:Number = 4; private var lastMsg:LogMessage; // The human-readable descriptions of the above log levels. public static var levelDescriptions = ["FATAL", "ERROR", "WARN", "INFO", "DEBUG"]; // The zero-relative filter level for the log. Messages with a level // above logLevel will not be passed on to observers. // Default is 3, "INFO" (only DEBUG messages are filtered out). private var logLevel:Number; /** * Logger Constructor */ private function Logger () { // Show "INFO" level messages by default. setLevel(Logger.INFO); } /** * Returns a reference to the log instance. * If no log instance exists yet, creates one. * * @return A Logger instance. */ public static function getLog():Logger { if (log == null) { log = new Logger(); } return log; } /** * Returns a human readable string representing the specified log level. */ public static function getLevelDesc(level:Number):String { return levelDescriptions[level]; } /** * Sets the message filter level for the log. * * @param lev The level above which messages are filtered out. */ public function setLevel(lev:Number):Void { // Make sure the supplied level is an integer. lev = Math.floor(lev); // Set the log level if it's one of the acceptable levels. if (lev >= Logger.FATAL && lev <= Logger.DEBUG) { logLevel = lev; info("Log level set to: " + lev); return; } // If we get this far, the log level isn't valid. warn("Invalid log level specified."); } /** * Returns the message filter level for the log. */ public function getLevel():Number { return logLevel; } /** * Returns the most recent message sent to the log. */ public function getLastMsg():LogMessage { return lastMsg; } /** * Sends a message to the log, with severity "FATAL". */ public function fatal(msg:String):Void { // If the filter level is at least "FATAL", broadcast the message to observers. if (logLevel >= Logger.FATAL) { // Construct the log message object. lastMsg = new LogMessage(msg, Logger.FATAL); // Pass the message on to observers. setChanged(); notifyObservers(lastMsg); } } /** * Sends a message to the log, with severity "ERROR". */ public function error(msg:String):Void { // If the filter level is at least "ERROR", broadcast the message to observers. if (logLevel >= Logger.ERROR) { lastMsg = new LogMessage(msg, Logger.ERROR); setChanged(); notifyObservers(lastMsg); } } /** * Sends a message to the log, with severity "WARN". */ public function warn(msg:String):Void { // If the filter level is at least "WARN", broadcast the message to observers. if (logLevel >= Logger.WARN) { lastMsg = new LogMessage(msg, Logger.WARN); setChanged(); notifyObservers(lastMsg); } } /** * Sends a message to the log, with severity "INFO". */ public function info(msg:String):Void { // If the filter level is at least "INFO", broadcast the message to observers. if (logLevel >= Logger.INFO) { lastMsg = new LogMessage(msg, Logger.INFO); setChanged(); notifyObservers(lastMsg); } } /** * Sends a message to the log, with severity "DEBUG". */ public function debug(msg:String):Void { // If the filter level is at least "DEBUG", broadcast the message to observers. if (logLevel >= Logger.DEBUG) { lastMsg = new LogMessage(msg, Logger.DEBUG); setChanged(); notifyObservers(lastMsg); } } }

    using the Logger in a movie

    to use Logger, first get a reference to the Singleton instance:

    import logger.*; var log:Logger; log = Logger.getLog();

    then create observers:

    var outputLogView:OutputPanelView; var textLogView:TextFieldView; outputLogView = new OutputPanelView(log); textLogView = new TextFieldView(log, this, 0, 50, 50, 400, 200);

    then register observers to receive updates:

    log.addObserver(outputLogView); log.addObserver(textLogView);

    then send messages to the log:

    log.fatal("This is a non-recoverable problem."); log.error("This is a serious problem that may be recoverable."); log.warn("This is something to look into, but probably isn't serious."); log.info("This is a general bit of application information.");

    this message won't appear because the default log level is INFO:

    log.debug("This is a note that helps track down a bug.");

    but if we change the log level to DEBUG...

    log.setLevel(Logger.DEBUG);

    ...then debug messages appear

    log.debug("Just testing!");

    summary

    design patterns:

  • make you more productive
  • help you write cleaner code
  • Observer and Singleton are just two of the many available

    if you like design patterns, try these resources:

  • GoF book -- Design Patterns: Elements of Reusable Object-oriented Software
  • design pattern examples in Java, see Design Patterns in Java Reference and Example Site