overview
learn what a design pattern is
use two design patterns:
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:
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:
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 objectsaddObserver()
, adds an observer to the observers
arrayremoveObserver()
, removes an observer from the observers
arraynotifyObservers()
, sends update notifications to all observersthe 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:
WeatherReporter.getTemperature()
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:
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:
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:
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:
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
private function Logger () { // Show "INFO" level messages by default. setLevel(Logger.INFO); }
private static var log:Logger = null;
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:
lastMsg
)LogMessage
)setChanged()
)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:
Observer and Singleton are just two of the many available
if you like design patterns, try these resources: