< Day Day Up > |
Now that we've implemented the core of the Observer pattern, let's put it to use in a real-world example ”an application log. To create the log, we'll define four classes: Logger , LogMessage , OutputPanelView , and TextFieldView , all of which reside in the logger package. The Logger class is our subject. The OutputPanelView and TextFieldView classes are our observers. The LogMessage class is our info object. Any class in the application can send a message to the Logger class. The receipt of a new message constitutes a change in the Logger class's state, causing it to broadcast the change (i.e., the message) to all Logger observers. In our case, the Logger observers are OutputPanelView and TextFieldView . The OutputPanelView renders log messages to the Output panel in Flash's Test Movie mode. The TextFieldView renders log messages to a text field on the movie's Stage, allowing them to be seen at runtime in the Flash Player. Our Logger class uses the push model to broadcast messages. When a message arrives, Logger creates a LogMessage instance and passes that instance on to its list of observers. Each LogMessage provides methods for retrieving the text and severity of the message logged. LogMessage instances can have one of five severities, represented by the integers 0 through 4, as follows : FATAL (0), ERROR (1), WARN (2), INFO (3), and DEBUG (4). The severity-level integers are stored in static properties of the Logger class. They can be converted to human-readable strings via the Logger.getLevelDesc( ) method. The Logger class provides a filter for suppressing log messages. Using Logger.setLevel( ) , a Logger instance can set a severity level, which determines whether messages should be broadcast or discarded. Messages with a severity level greater than the Logger 's severity level are not broadcast to observers. For example, if a message has a severity of 4 (DEBUG), but the Logger 's severity level is 3 (INFO), the message is not broadcast. The Logger class's filter lets us easily change the quantity and granularity of messages of a log from a central location. During development, we might use a log severity level of 4 (broadcast all messages), but in the final application, we might use a log severity level of 1 (broadcast ERROR and FATAL messages only). 16.2.1 The LogMessage ClassLet's start our examination of our log's source code with the LogMessage class, a simple class for setting and retrieving the text and severity of a logged message. The LogMessage class defines the following members :
As we'll see later, the Logger class creates LogMessage instances when broadcasting a change notification to its observers. Example 16-3 shows the source code for the LogMessage class. Example 16-3. The LogMessage class/** * A log message. Sent by the Logger instance to all registered * log observers when a new log message is generated. */ class logger.LogMessage { // The text of the message sent to the log. private var msg:String; // The severity level of this message. private var level:Number; /** * LogMessage Constructor */ public function LogMessage (m:String, lev:Number) { setMessage(m); setLevel(lev); } /** * Sets the log message. */ public function setMessage (m:String):Void { msg = m; } /** * Returns the log message. */ public function getMessage ( ):String { return msg; } /** * Sets the severity level for this message. */ public function setLevel (lev:Number):Void { level = lev; } /** * Returns the severity level for this message. */ public function getLevel ( ):Number { return level; } } Now let's move on to OutputPanelView , a class that receives LogMessage instances from Logger and generates corresponding on-screen messages. 16.2.2 The OutputPanelView ClassThe OutputPanelView class displays log messages in the Output panel in Flash's Test Movie mode. It implements the Observer interface from Example 16-2 and defines only one property and one method:
By convention, all observer classes should store an instance of the subject they are observing. They use that reference to pull changes from the subject or to set the subject's state. In our case, the Logger class broadcasts its updates using the push model, so the log property in OutputPanelView is not actually used. However, we maintain it as a matter of good form and for the sake of possible future updates to the OutputPanelView class. Example 16-4 shows the source code for the OutputPanelView class. Pay special attention to the update( ) method, which receives a LogMessage instance as an argument and uses it to display log messages in the Output panel. Notice that the generic infoObj instance received by update( ) is cast to the LogMessage datatype before it is used so that the compiler can perform type checking on it. (Technically, the cast is not necessary in this case because the Object class is dynamic, so invoking non- Object methods on infoObj would not cause an error.) Example 16-4. The OutputPanelView classimport util.Observer; import util.Observable; import logger.Logger; import logger.LogMessage; /** * An observer of the Logger class. When a movie is played in * the Flash authoring tool's Test Movie mode, this class displays * log messages in the Output panel. */ class logger.OutputPanelView implements Observer { // The log (subject) that this object is observing. private var log:Logger; /** * Constructor */ public function OutputPanelView (l:Logger) { log = l; } /** * Invoked when the log changes. For details, see the * Observer interface. */ 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); } } Now let's look at our log's other observer class, TextFieldView . 16.2.3 The TextFieldView ClassThe TextFieldView class displays log messages in a text field in the Flash movie rather than in the Output panel. The basic structure of TextFieldView is identical to OutputPanelView . Like OutputPanelView , TextFieldView defines a log property and an update( ) method. It also adds a new method, makeTextField( ) , which creates an on-screen TextField instance in which to display messages. And it adds a new property, out , which stores a reference to the text field created by makeTextField( ) . The TextFieldView constructor defines seven parameters:
Example 16-5 shows the code for the TextFieldView class. Once again, pay special attention to the update( ) method, which receives log messages and handles the important task of displaying them on screen. Example 16-5. The TextFieldView classimport util.Observer; import util.Observable; import logger.Logger; import logger.LogMessage; /** * An observer of the Logger class. This class displays * messages sent to the log in an on-screen text field. */ class logger.TextFieldView implements Observer { // The log that this object is observing. private var log:Logger; // A reference to the text field. private var out:TextField; /** * TextFieldView Constructor */ 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); } /** * Invoked when the log changes. For details, see the * Observer interface in Example 16-2. */ public function update (o:Observable, infoObj:Object):Void { // Cast infoObj to a LogMessage instance for type checking. var logMsg:LogMessage = LogMessage(infoObj); // Display the log message in the log text field. out.text += Logger.getLevelDesc(logMsg.getLevel( )) + ": " + logMsg.getMessage( ) + "\n"; // Scroll to the bottom of the log text field. out.scroll = out.maxscroll; } /** * Creates a text field in the specified movie clip at * the specified depth. Log messages are displayed in the text field. */ public function makeTextField (target:MovieClip, depth:Number, x:Number, y:Number, w:Number, h:Number):Void { // Create the text field. target.createTextField("log_txt", depth, x, y, w, h); // Store a reference to the text field. out = target.log_txt; // Put a border on the text field. out.border = true; // Make the text in the text field wrap. out.wordWrap = true; } public function destroy ( ):Void { log.removeObserver(this); out.removeTextField( ); } } The completion of the TextFieldView class is a eureka! moment in which we can clearly see the fruits of our labor. With both the TextFieldView and OutputPanelView classes implemented, we now have two separate displays based on the same information source. When the Logger class receives a message, it doesn't have to worry about how the message is rendered. Instead, it merely broadcasts the message to its observers. The rendering and processing of the messages are handled by two completely separate observer classes. In our current example, we render the log in two ways, but once the general logging system is in place, it is trivial to add more log-rendering classes. For example, we could add a class that sends the log to a server-side database. Or we could add a class that archives the log locally and provides searching and arbitrary access to log messages. Each class neatly encompasses its own responsibilities. And the Logger class doesn't care whether there are three log-processor (observer) classes, a hundred such classes, or none. Now let's put the final piece in the Observer puzzle, the Logger class. 16.2.4 The Logger ClassAs the subject of our Observer implementation, the Logger class extends the Observable class, using it to handle the grunt work of managing observers and broadcasting state changes (in this case, log messages). Here's the skeleton for the Logger class: class logger.Logger extends Observable { } To ensure that each application creates only one Logger instance, we use the Singleton design pattern, which we'll study in the next chapter. The following aspects of the Logger class are all part of the Singleton design pattern; we'll skip consideration of these items for now and return to them next chapter:
As we learned earlier, the Logger class maintains a log severity level that is used to filter log messages. The severity levels are stored in static properties, as follows: 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; Human-readable strings describing the severity levels are stored in the property levelDescriptions : public static var levelDescriptions = ["FATAL", "ERROR", "WARN", "INFO", "DEBUG"]; The current severity level is stored in an instance property, logLevel : private var logLevel:Number; To allow the log severity level to be set, the Logger class defines a setLevel( ) method: public function setLevel(lev:Number):Void { lev = Math.floor(lev); if (lev >= Logger.FATAL && lev <= Logger.DEBUG) { logLevel = lev; info("Log level set to: " + lev); return; } warn("Invalid log level specified."); } To allow the log severity level to be retrieved, the Logger class defines a getLevel( ) method: public function getLevel( ):Number { return logLevel; } A human-readable string representing a given log level can be retrieved via the class method getLevelDesc( ) : public static function getLevelDesc(level:Number):String { return levelDescriptions[level]; } By default, the Logger constructor sets each Logger instance's severity level to 3 (INFO): private function Logger ( ) { setLevel(Logger.INFO); } And now the code we've been waiting for. To allow messages to be sent to the log, the Logger class provides five methods, corresponding to the five log levels: fatal( ) , error( ) , warn( ) , info( ) , and debug( ) . To send a message to the log, we first create a Logger instance: var log:Logger = Logger.getLog( ); Then we pass the message to be sent to the desired message-sending method. For example, the following code sends the message, "Something went wrong," with a severity of ERROR: log.error("Something went wrong"); The following code sends a message, "Application started!" with a severity of INFO: log.info("Application started!"); The most recent message sent to the log is stored in the instance property lastMsg : private var lastMsg:LogMessage; The methods fatal( ) , error( ) , warn( ) , info( ) , and debug( ) are all structured identically. Let's look at the code for the info( ) method to see how a log message is handled. Remember that the five message-sending methods are the state-change methods of the subject in our Observer pattern. As such, they follow a basic structure that all state-change methods in an Observer implementation follow. Here's the code for info( ) , with comments explaining each line: public function info(msg:String):Void { // If the filter level is at least "INFO"... if (logLevel >= Logger.INFO) { // ...then broadcast the message to observers. // Using the supplied message string ( msg ), // create a LogMessage instance to send to observers. // The LogMessage instance is the info object of this // notification. Store the LogMessage instance in // lastMsg for later retrieval. lastMsg = new LogMessage(msg, Logger.INFO); // Indicate that the subject has changed state. setChanged( ); // Use notifyObservers( ) to invoke update( ) on all Logger // observers, passing the LogMessage instance as an argument. // For the source code of notifyObservers( ) , see Example 16-1. notifyObservers(lastMsg); } } The basic structure of the Logger.info( ) method is:
All state-change methods in Observable subclasses use the preceding structure. Example 16-6 shows the code for Logger in its entirety. Example 16-6. The Logger classimport 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 preceding 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 are not 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); } } } Notice that the five message-sending methods in the Logger class ” fatal( ) , error( ) , warn( ) , info( ) , and debug( ) ” all contain nearly identical code. In this example, we'll accept that redundancy in order to repeatedly demonstrate the general structure of a state-change method. However, in a real-world version of the Logger class, we'd move the repeated code to a centralized method that checks the log level and notifies observers when appropriate. We'd call our centralized method handleMessage( ) and have each of the message-sending methods use it to validate and broadcast messages, as shown next: // The new handleMessage( ) method. public function handleMessage(msg:String, msgSeverity:Number):Void { if (logLevel >= msgSeverity) { lastMsg = new LogMessage(msg, msgSeverity); setChanged( ); notifyObservers(lastMsg); } } // Here's a revised message-sending method, debug( ) , // showing how handleMessage( ) would be used. (Other // message-sending methods are not shown.) public function debug(msg:String):Void { handleMessage(msg, Logger.DEBUG); } 16.2.5 Inheritance Misuse?As we've just seen, in our implementation of the Logger class, we extend Observable . In our situation, the Logger class doesn't need to inherit from any other class, so it can extend Observable without issue. But what if the Logger class were already a subclass of some other class (say, MessageManager )? It wouldn't be able to inherit from the Observable class! Here, we encounter a classic misuse of inheritance ”extending a class simply to borrow its functionality. We first saw this kind of misuse in Chapter 6 and later showed how to avoid it in Chapter 8, under "Multiple Type Inheritance with Interfaces." Specifically, the Logger class doesn't have a legitimate "Is-A" relationship with the Observable class. That is, the Logger class is not a specialized kind of update broadcaster . Other classes won't use it in place of Observable for its additional broadcast features. On the contrary, the Logger class manages an application log and just happens to need the update broadcasting functionality found in Observable . In short, we've used inheritance to arrange a marriage of convenience between the Logger and Observable classes. In a much more flexible implementation of the Observer pattern, we would define the Observable datatype as an interface, not as a concrete class. We would then create an implementation of that interface in a class named ObservableSubject . The ObservableSubject class would have all the features of the Observable class from Example 16-1. Our Logger class would not extend ObservableSubject . Instead, it would implement the Observable interface and make use of ObservableSubject via composition (exactly as the Rectangle class from Chapter 8 implemented Serializable and used the Serializer class via composition). The Logger class would then be able to inherit from a more natural superclass (again, perhaps a generic MessageManager class). So, knowing that we're misusing inheritance in our Logger example, should we now alter our Observer implementation to allow subjects such as Logger to use a base class such as ObservableSubject via composition instead of inheritance? That depends on the situation. The composition approach is undeniably more flexible (adding justification to Sun's recommendation that every class that can be extended should also be usable via composition!). However, the composition approach is also more complex. In a simple application, that complexity can translate to unnecessary development time and source code that's harder to digest. Hence, a good rule to follow is one we've seen several times already: Never Add Functionality Early (for details on this and other Extreme Programming rules, see http://www.extremeprogramming.org/rules.html). So far our Logger class doesn't need to inherit from any other class, so we don't need to worry that we're "misusing" inheritance. We can wait until the program we're creating requires another subject that cannot inherit from Observable because it already inherits from another class. That's a good time to implement the composition version of Observer. In this chapter, we'll stick with our inheritance-based Observer implementation. For comparison, however, Example 16-7 presents the composition-based version. Differences from the inheritance version are shown in bold. Refer to Chapter 8 for an explanation of the basic structure of composition. Note that the composition-based implementation of Observer uses, verbatim, the previous versions of the Observer interface and the LogMessage , OutputPanelView , and TextFieldView classes. The source code for those items is, hence, not repeated in Example 16-7. You can download the code shown in Example 16-7 from http:// moock .org/eas2/examples. Example 16-7. Implementing the Observer pattern using composition// Code in Observable.as . This is the new Observable interface. import util.Observer; interface util.Observable { public function addObserver(o:Observer):Boolean; public function removeObserver(o:Observer):Boolean public function notifyObservers(infoObj:Object):Void public function clearObservers( ):Void; public function hasChanged( ):Boolean; public function setChanged( ):Void; public function clearChanged( ):Void; public function countObservers( ):Number; } // Code in ObservableSubject.as . The ObservableSubject class is nearly // identical to the previous Observable class, save for the commented // sections in bold. Note that ObservableSubject implements the new // Observable interface. Hence, the Observable interface must be imported. import util.Observer; import util.Observable; class util.ObservableSubject implements Observable { private var changed:Boolean = false; private var observers:Array; // Constructor function, this time named ObservableSubject . public function ObservableSubject ( ) { observers = new Array( ); } public function addObserver(o:Observer):Boolean { if (o == null) { return false; } for (var i:Number = 0; i < observers.length; i++) { if (observers[i] == o) { return false; } } observers.push(o); return true; } public function removeObserver(o:Observer):Boolean { 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; } public function notifyObservers(infoObj:Object):Void { if (infoObj == undefined) { infoObj = null; } if (!changed) { return; } var observersSnapshot:Array = observers.slice(0); clearChanged( ); for (var i:Number = observersSnapshot.length-1; i >= 0; i--) { observersSnapshot[i].update(this, infoObj); } } public function clearObservers( ):Void { observers = new Array( ); } public function setChanged( ):Void { changed = true; } public function clearChanged( ):Void { changed = false; } public function hasChanged( ):Boolean { return changed; } public function countObservers( ):Number { return observers.length; } } // Code in Logger.as. import util.Observable; // Import the new Observable interface. import util.ObservableSubject; // Import the new ObservableSubject class. import util.Observer; import logger.LogMessage; // Implement Observable , but don't extend ObservableSubject ! class logger.Logger implements Observable { // An ObservableSubject instance, used to broadcast updates to observers. private var subj:ObservableSubject; private static var log:Logger = null; 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; public static var levelDescriptions = ["FATAL", "ERROR", "WARN", "INFO", "DEBUG"]; private var logLevel:Number; // Create the ObservableSubject instance in the constructor. private function Logger ( ) { subj = new ObservableSubject( ); setLevel(Logger.INFO); } public static function getLog( ):Logger { if (log == null) { log = new Logger( ); } return log; } public static function getLevelDesc(level:Number):String { return levelDescriptions[level]; } 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."); } public function getLevel( ):Number { return logLevel; } public function getLastMsg( ):LogMessage { return lastMsg; } public function fatal(msg:String):Void { if (logLevel >= Logger.FATAL) { // Construct the log message object. lastMsg = new LogMessage(msg, Logger.FATAL); // Pass the message on to observers. setChanged( ); notifyObservers(lastMsg); } } public function error(msg:String):Void { if (logLevel >= Logger.ERROR) { lastMsg = new LogMessage(msg, Logger.ERROR); setChanged( ); notifyObservers(lastMsg); } } public function warn(msg:String):Void { if (logLevel >= Logger.WARN) { lastMsg = new LogMessage(msg, Logger.WARN); setChanged( ); notifyObservers(lastMsg); } } public function info(msg:String):Void { if (logLevel >= Logger.INFO) { lastMsg = new LogMessage(msg, Logger.INFO); setChanged( ); notifyObservers(lastMsg); } } public function debug(msg:String):Void { if (logLevel >= Logger.DEBUG) { lastMsg = new LogMessage(msg, Logger.DEBUG); setChanged( ); notifyObservers(lastMsg); } } // Wrapper methods for ObservableSubject methods follow. These methods // subcontract their work out to the ObservableSubject instance, subj . public function addObserver(o:Observer):Boolean { return subj.addObserver(o); } public function removeObserver(o:Observer):Boolean { return subj.removeObserver(o); } public function notifyObservers(infoObj:Object):Void { subj.notifyObservers(infoObj); } public function clearObservers( ):Void { subj.clearObservers( ); } public function setChanged( ):Void { subj.setChanged( ); } public function clearChanged( ):Void { subj.clearChanged( ); } public function hasChanged( ):Boolean { return subj.hasChanged( ); } public function countObservers( ):Number { return subj.countObservers( ); } } 16.2.6 Using the Logger ClassNow that we've seen how our subject ( Logger ) and its observers ( OutputPanelView and TextFieldView ) work individually, let's put them all together to form a functional logging system. The code in this section could go on a frame in the timeline of a .fla file or in a class (an .as file). Furthermore, the code works equally well with both the inheritance-based and composition-based implementations of the Observer pattern shown in this chapter. First, we import the logger package (so we can refer to the Logger , LogMessage , OutputPanelView , and TextFieldView classes in that package directly): import logger.*; Then, we create a variable, log , to store our application's Logger instance: var log:Logger; Then we create the Logger instance: log = Logger.getLog( ); Next, we create two variables to store our Logger observers: var outputLogView:OutputPanelView; var textLogView:TextFieldView; Then we create our observer instances, passing each the Logger instance for this application: outputLogView = new OutputPanelView(log); textLogView = new TextFieldView(log, someMovieClip, 0, 50, 50, 300, 200); Finally, we use addObserver( ) to register our observers to receive updates from the Logger instance: log.addObserver(outputLogView); log.addObserver(textLogView); Our log's ready to go! Let's now try logging some messages: 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."); log.debug("This is a note that helps track down a bug."); If we executed the preceding code, the debug( ) message wouldn't appear because the Logger instance filters out debug( ) messages by default. To enable all messages for the log, we'd use: log.setLevel(Logger.DEBUG); If you want to see the log work on your own computer, you can download the example files from http://moock.org/eas2/examples. |
< Day Day Up > |