Observer Pattern

From HEWIKI
Jump to: navigation, search

Contents

The Observer Pattern (sometimes called Listener Pattern) is a design pattern which establishes a one-to-many relationship between objects, such that changes to one object (the subject) may be communicated to many other objects (the observers/listeners). The power of this pattern is that it loosely couples objects together in a manner that does not require the subject or its listeners understanding anything about the other, beyond the fact that they implement a shared interface for communicating.

For Clean Engine, the primary example of an implementation of the Observer Pattern is supported by two abstract classes, obsSubject and obsListener. It is these classes that are used as examples for this page.

Please note, the implementation of the obsSubject/obsListener classes is just one concrete implementation of the Observer Pattern, but others are possible.

What problem(s) does this solve?

What problem(s) does this not solve?

Concepts

Establish one-to-many relationships

ObserverPatternOneToManyRelationShip.jpg

The Observer Pattern is used to establish one-to-many relationships between objects in a loosely coupled manner.

There is also one subject with zero or more (0...N) listeners.

Note also that a listener may have more than one subject to which it is "subscribed".

Listeners = Observers

Depending on the source, Observer is often used interchangeably with Listener.

Loose coupling

As an example, imagine the smart home of the future, where objects in your house know how to communicate changes in their states, to other objects that are interested in those states. When you stick your key into the door, unlock it and open it, the door object's state changes from locked to unlocked, and notifies the change to objects that have "subscribed" as observers.

Each observer can then do whatever it wants to do upon notification:


All of this is accomplished without the door object having any idea of what will happen, or how it happens when its state changed from locked to unlocked.

Loose coupling 
describes a relationship between two or more objects/systems with some kind of shared semantic framework whereby messages can be passed from sender to recipient. Loose coupling reduces the chance that changes to the structure of one object/system will require changes be made to other objects/systems.


obsSubject and obsListener are Abstract Classes

UMLobsSubjectobsListener.jpg

The obsSubject and obsListener classes are abstract classes from which other classes inherit, and their class method scripts override methods as needed to achieve the desired functionality. There are a few concrete classes implemented for Clean Engine that function as proxies for a script or node to handle the notification in particular ways.

For example, an instantiation of obsListenerScript acts as a proxy for an HSL Script, since scripts are not "objects" and as such you may not call a method on a script. The obsListenerScript knows how to call a particular shared function in the script for which it acts as a proxy.

Many objects do not need to have an intermediate object to act as their proxy. For Clean Engine, most objects that wish to receive notification from a Subject are created from a child class of obsListener and implement the EventRaised() method in their own class method script.


Usage

This section assumes you have decided to use the generic implementation of the Observer Pattern in Clean Engine as implemented by the obsSubject/obsListener classes. If your developers have a implementation they prefer you use, please see them for usage.

Making your Class a "Subject"

UMLobsSubject.jpg
Assume you have an object that you wish to have produce events to which other objects listen, if you knew that there was only a particular object that needed notification you could simply write a call to that object in script. This simple solution works very well...until you have multiple objects that may be interested in which case you have to edit the script and add each one (still not an impossible situation). Now imagine all of those objects may want to sometimes receive notification and sometimes not, or someone writes a new system that needs to listen.

The Observer Pattern is the classic solution for our problem, and conveniently in HeroEngine you can leverage the generic implementation we made by making your class a child of the obsSubject class using either the DOM Editor or the CLI.

We strongly advise against using GLOMming to make an object a subject dynamically, as doing so seems like it would almost certainly logically flawed.


Making a custom Listener class

UMLobsListener.jpg
Often you will want to subscribe an existing node directly as a "listener" instead of creating a proxy listener node, this requires less memory(one less node) and removes a layer of indirection (less processing). Other times you may create a custom listener that does act as a proxy for some other node or system, following the conceptual pattern expressed by the obsListenerNode and obsListenerScript classes.

The creation of a custom listener class requires nothing more than making your class a child of the obsListener class using either the DOM Editor or the CLI. Once established as a child of obsListener there are at least two methods you will almost certainly wish to override in your class methods script.

We strongly advise against using GLOMming to make an object a listener dynamically, as doing so seems like it would almost certainly logically flawed.

Additional usage for these methods is found below.


Creating a Listener

If you do not already have an object that is a child of obsListener, there are several ways you might create a listener.

Built-in Listener proxies

There are four classes the obsSubject class knows how to use to create proxy objects that are listeners. Each class has slightly different behavior, but the fundamental nature of receiving an event and sending notification to something else (node, script, etc) is the same.

The Node Listener (obsListenerNode)

CautionSign.gif By default, the obsListenerNode class does not allow you to subscribe the same node multiple times to the same subject.

This behavior eliminates the most common problem novice users encounter, subscribing multiple times and then receiving multiple event notifications.

The node listener is a proxy object that knows the ID of another node that wishes to receive notification of an event.

When a node listeners receives an event (in its EventRaised() method) it processes the following logic:

// assuming you have a reference to the subject
//   the second parameter is a boolean indicating whether the listener should be a 
//   persisted node or not.  Generally you do not want persisted listeners.
var listener = subject.createNodeListener( nodeToNotifyID, false )
 
// After creating, you need to subscribe it to the subject
subject.addListener( listener )
(obsListenerClientNode)

A child class of the obsListenerNode (existing on the server only) provides for the same functionality as the normal node listener, but manages the remote call required to send the event to the client. It has the fundamental assumption that the data object being passed is comprised of classes that are defined in both the server and client GOMs.

// assuming you have a reference to the subject
//   the third parameter is a boolean indicating whether the listener should be a persisted node or not
//   most of the time you do not want persisted listeners.
var listener = subject.createClientNodeListener( targetClientID, nodeToNotifyID, false )
 
// After creating, you need to subscribe it to the subject
subject.addListener( listener )

The Script Listener (obsListenerScript)

CautionSign.gif By default, the obsListenerScript class does not allow you to subscribe the same script multiple times to the same subject.

This behavior eliminates the most common problem novice users encounter, subscribing multiple times and then receiving multiple event notifications.

Script listeners function as proxy objects for a script (since scripts are not GOM objects). When a script listener receives the EventRaised() method, it calls the shared function EventRaisedNotify in the script for which it is a proxy.

// assuming you have a reference to the subject
//   the second parameter is a boolean indicating whether the listener 
//   should be a persisted node or not. Generally you do not want persisted listeners.
var listener = subject.createScriptListener( scriptToNotify, false )
 
// After creating, you need to subscribe it to the subject
subject.addListener( listener )


(obsListenerClientScript)

A child class of the obsListenerScript (existing on the server only) provides for the same functionality as the normal script listener, but manages the remote call required to send the event to the client. It has the fundamental assumption that the data object being passed is comprised of classes that are defined in both the server and client GOMs.

// assuming you have a reference to the subject
//   the third parameter is a boolean indicating whether the listener should be a persisted node or not
//   most of the time you do not want persisted listeners.
var listener = subject.createClientScriptListener( targetClientID, scriptToNotify, false )
 
// After creating, you need to subscribe it to the subject
subject.addListener( listener )

Custom Listener class

If you have created a custom listener class that your system wishes to use, simply instantiate a node from your custom class setting any required fields and add the listener normally.

// Assuming you have a reference to the subject
 
var customListener = createNodeFromClass( "CustomListenerClass" )
 
subject.addListener( customListener )

Adding a Listener to a subject

CautionSign.gif The proxy listeners by default do not allow you to subscribe multiple listeners for the same node/script to the same subject.

If you need the capability to have multiple notifications to the same proxy target, create a child class of the appropriate obsListener class overriding the IsAlreadyListening() method.

The Observer Pattern supports the dynamic expression of interest in a given subject, which means a given listener may (un)subscribe to a subject at will. For the obsSubject implementation, subscription to the subject is achieved using the addListener() method.

Adding a listener to a subject will result in the listener becoming hard associated to the subject unless it already has a hard association to another node. That means that the subject "owns" its listeners unless they are "owned" by some other node as represented by a hard association.

// assuming you have a reference to the subject and a listener node
subject.addListener( listener )


Removing a Listener from a subject

The Observer Pattern supports the dynamic expression of interest in a given subject, which means a given listener may (un)subscribe to a subject at will. For the obsSubject implementation, unsubscription to the subject is achieved using the removeListener() method.

Removing a listener will result in the subject calling the destroyListener() method on the listener if it is associated to the subject via a hard association (IE if the subject "owns" the listener it will attempt to clean it up).

// assuming you have references to both subject and listener
subject.removeListener( listener )

An alternative to this is calling the listener's deleteListener() method, which results in removeListener() being called on all subjects to which the listener is subscribed.

// an alternative to calling removeListener on a particular subject is to call the listener's
//   deleteListener method
listener.deleteListener()

Raising an event

The Event System involves the obsSubject/obsListener implementation of the Observer Pattern, which passes a generic noderef during the raising of an event. Commonly clean engine systems use a generic event class EventObject, which has fields for event type, caused by ID and affects ID. Any object(node) may be passed as the data for an event, however there is an implied contract with a particular subject's listeners to pass data in a consistent manner.

When an event is raised by calling notifyListeners() the following logic processes:

// assuming you are using the generic event object
eObject as noderef of class eventObject = CreateNodeFromClass( "eventObject" )
eObject.eventType = "SOMEEVENT"
 
// the caused by and affects ids are optional but are so commonly needed that they are
//   including in this base class.  Some listeners (like the obsListenerNode) expect the
//   eventAffectsID to be set so they can determine if the event is one in which they are interested
eObject.eventCausedByID = someNode
eObject.eventAffectsID = someNode
 
// The setChanged() method must be called or the subject will do nothing when
//   notifyListeners is called.  This supports subjects that wish to implement a
//   timed push, or pull sytles for notification.
//
subject.setChanged()
subject.notifyListeners( eObject )

If you are paying close attention to the same code, you may have noticed that we are creating a new instantiation every time we generate an event. You may find this to be less than optimal depending on the volume of events, whether or not they share common fields such as the eventObject, the needs of your system, etc. Some Clean Engine systems reuse the same instantiation over and over through a Singleton implementation.

Advanced usage

Optimized implementations

Avoiding instantiating an event object for each event

Instead of creating an event object for each event that is raised, you may wish to implement a Singleton eventObject for your system. This reduces the overhead from node instantiation to a one-time hit, to avoid automatic cleanup by the obsSubject use a hard association to associate the event object (target) to some well known node (source) (for clean engine this well known node is often a System Node.

Custom implementation of the pattern

The generic implementation of the obsSubject/obsListener classes is extremely flexible and easy to use, however it is possible for certain extremely high performance applications you may wish to have your developers implement a version of the Observer Pattern that passes event ENUMs or IDs instead of nodes.

Another potential optimization, but less flexible, would be to eliminate listeners as instantiations and maintain lists internally by the subject.

Reference

Personal tools
Namespaces
Variants
Actions
Navigation
Toolbox