Callbacks and Events


Callbacks and events are necessary because of how programs can be structured. For example, you could program a subsystem that needs to be extended or is based on functionality not defined by the subsystem. In either case, a user would define an implementation and then pass a reference of that implementation to the subsystem. The implementation of this would use the Commons Bridge, where the subsystem defines the interface and the user defines the implementation.

A Simple Implementation of a Callback

Listing 6.1 shows an example of a kernel that exposes some interfaces.

Listing 6.1
start example
 interface Operation { public long doSomething( long num1, long num2); } interface Result { public void setResult( long result); } class SubSystem { private Operation _operation = null; private Result _result = null; public SubSystem( Operation op, Result result) { _operation = op; _result = result; } public void operation( long num1, long num2) { _result.setResult( _operation.doSomething(num1,num2)); } } 
end example
 

In Listing 6.1, two interfaces ” Operation and Result ”are defined. We've defined two interfaces to show how a subsystem will execute only a generic process; it will not attempt to translate what the generic process is trying to do. The class SubSystem in Listing 6.1 defines a subsystem used to process two numbers . This class is responsible for binding together two pieces of functionality into one process. In Listing 6.1, the interface Operation is responsible for performing some kind of operation on two numbers. The Commons Bridge has already demonstrated this strategy on code abstraction.

What is new in Listing 6.1 is that the interface Result is used to display a result. Yes, the interface Result and associated implementations are implemented using the Commons Bridge. However, the semantics of the interface Result are very different from those of the interface Operation . The interface Result receives data asynchronously and processes it. The interface Operation is very obviously used to process two numbers and to return a result. However, the interface Result receives a result and has to do something with it. You may notice that the statement "has to do something with it" is very vague. Well, it's vague because, unlike with a task-driven interface, an event-driven or asynchronous interface indicates that something has occurred. Once something has occurred, it is the responsibility of the interface implementation to do something.

Notice how simple and elegant the class method SubSystem.operation is in Listing 6.1. The method is not responsible for the input, output, or processing. It's responsible only for combining the various interfaces and operations into something that relates to a task. This is very significant because it indicates that a micro kernel-type architecture is created. Listing 6.2 is a sample implementation of the two interfaces.

Listing 6.2
start example
 class MyOperation implements Operation { public long doSomething(long num1, long num2) { return num1 + num2; } } class MyResult implements Result { public void setResult(long result) { System.out.println( "The result is (" + result +  ")"); } } 
end example
 

In Listing 6.2, the class MyResult implements the interface Result. The class MyResult outputs the result to the system standard output. This is a simple way of displaying the output. The code shown in Listings 6.1 and 6.2 can be wired together and utilized as shown in Listing 6.3.

Listing 6.3
start example
 SubSystem subsys = new SubSystem( new MyOperation(), new MyResult()); subsys.operation( 1, 3); 
end example
 

Listing 6.3 shows how simple it is to instantiate the classes MyOperation and MyResult and let the SubSystem do the rest of the work. However, this simplicity contains a hidden trap, which relates to how the classes MyOperation and MyResult are structured.

Writing Maintainable Asynchronous Code

The problem of an asynchronous or event-driven application is that when the callback happens, the original context is lost. The class MyResult does not have a reference back to the class MyOperation . The problem of cross-referencing is best illustrated in typical GUI applications. A typical GUI application has handlers to react to specific events, like button clicks and list box selections. When reacting to events, the handlers will need to reference other Java classes that are not local to the handler class. Look back to Listing 6.2; the class MyResult may need to reference some data in the class MyOperation . The simplest solution to this problem is to change the implementations to Listing 6.4.

Listing 6.4
start example
 class EverythingInOne implements Operation, Result { // other stuff left out for simplicity } 
end example
 

In Listing 6.4, the class EverythingInOne implements both the interfaces Operation and Result . This approach of implementing multiple interfaces with one class works, but it is not an ideal object-oriented solution. Implementing two interfaces does not break object-oriented design; what does is the overall solution to implement the interfaces in one class. That could result in an object implementing ten or more interfaces, which is not good object-oriented design.

A more typical solution is shown in Listing 6.5.

Listing 6.5
start example
 class MyOperation implements Operation { private Result _result; } class MyResult implements Result { private Operation _operation; } 
end example
 

In Listing 6.5, the solution is to have each class reference an instance of the interface that each class implements. This solution is adequate, but it typically leads to very complex and brittle classes because each class references too many other interface instances. This is a very typical problem when you're building GUI-based applications. Java can solve this problem very elegantly, as shown in Listing 6.6.

Listing 6.6
start example
 class MyOverallClass implements Operation { private class LocalMyResult implements Result { public void setResult(long result) { doOutput( "Result is (" + result + ")"); } } private void doOutput( String buffer) { System.out.println( buffer); } public Result newResult() { return new LocalMyResult(); } public long doSomething(long num1, long num2) { return num1 + num2; } } 
end example
 

In Listing 6.6, the nested class LocalMyResult shows the clever Java trick. Notice in the implementation of the method setResult that a method call to doOutput is made. The trick is that the method implementation for doOutput is in the class MyOverallClass . This trick shows that an inner class can reference data in the outer class. When you're writing GUI code or code that has callbacks, this would be a good implementation strategy.

Another way of expressing the same notation is to use anonymous classes, as shown in Listing 6.7.

Listing 6.7
start example
 class MyAnonymousClass implements Operation { private void doOutput( String buffer) { System.out.println( buffer); } public Result newResult() { return new Result() { public void setResult(long result) { doOutput( "Result is (" + result + ")"); } }; } public long doSomething(long num1, long num2) { return num1 + num2; } } 
end example
 

In Listing 6.7, an anonymous class is defined by the method newResult . The declaration of the class in the method would be identical to the inner class of Listing 6.6, except that the class has no identity. However, the class does have a type and is automatically cast to the interface Result . The anonymous class is the type Result because the interface Result is instantiated in the method of newResult . The syntax looks a bit odd because it is part function call, part class declaration, and part class instantiation. No matter. The declaration is a class that is instantiated and used like any other interface instance of type Result .

Using the Java Classes Observer and Observable

The technique we just described in Listings 6.1 to 6.7 is part of a pattern called the Observer. It is another way of explaining an event-driven architecture where the implementing class does not know when or how it is called. Within the Java class libraries you can use the classes Observer and Observable to create an event mechanism. In Listing 6.1, the class SubSystem could inform only one Result interface instance that something has occurred. If you wanted to be able to call up multiple Result interfaces, you would have to manage and maintain a collection. This is the purpose of the class Observable .

The class Observer is a common interface. All classes that want notifications must implement it, as shown in Listing 6.8.

Listing 6.8
start example
 class ResultObserver implements Observer { public void update(Observable o, Object arg) { Long result = (Long)arg; SubSystemObservable observable = (SubSystemObservable)o; } } 
end example
 

In Listing 6.8, the method update is called when a notification is generated. Think back to Listing 6.1 and how the interface Result was updated. In that example, the notification occurred when the class method Subsystem.operation called interface method Result.setResult . To notify the class ResultObserver , the class Observable is used, as shown in Listing 6.9.

Listing 6.9
start example
 class SubSystemObservable extends Observable { private Operation _operation = null; public SubSystemObservable( Operation op) { _operation = op; } public void operation( long num1, long num2) { long result = _operation.doSomething(num1,num2); Long longResult = new Long( result); setChanged(); notifyObservers( longResult); } } 
end example
 

In Listing 6.9, the method operation has changed quite a bit (as compared to Listing 6.1). In the implementation of the method operation , the method call to doSomething is still made. Only, this time the result value is assigned to the variable result. Then, the variable result is converted into an object of type Long , which is the variable longResult . This is a necessary step because the class Observable sends out the notifications using objects and not primitives. The call to method setChanged is necessary because it defines that the state has changed and therefore that it is OK to send a notification. To send a notification, the method notifyObservers is called. This method accepts as a single parameter the object that is sent to the receivers of the notification.

Look back to Listing 6.8 ”the method update would be called by the notification. The method update has two parameters. The first parameter is a reference to the class that extends the Observable class; the second is the object sent when the method notifyObservers is called. In Listing 6.8, you'll also see the type casts to the original data types that the notification sends.

The class Observable exposes the following methods , which manage the collection of observers:

  • addObserver : Adds an observer to the collection

  • notifyObservers : Notifies the observers about an event

  • deleteObservers : Deletes all the observers from the collection

  • deleteObserver : Deletes a specific observer from the collection

  • setChanged : Sets the dirty flag, which is the flag to indicate a changed state, to true , indicating that if the method notifyObservers is called, all of the observers in the collection will receive the event

  • clearChanged : Changes the dirty flag to false and none of the observers will receive a notification

  • hasChanged : Retrieves the status of the dirty flag

  • countObservers : Returns how many observers are in the collection

With the new sub-system class, Listing 6.3 changes to Listing 6.10.

Listing 6.10
start example
 SubSystemObservable subsys = new SubSystemObservable( new MyOperation()); subsys.addObserver( new ResultObserver()); subsys.operation( 1, 2); 
end example
 

Using the Java classes Observer and Observable , a developer can create a very effective notification system, or a publish and subscribe framework. You may have combed through the Internet and questioned the effectiveness of these classes because all notification classes need to implement the interface Observer . Even so, that is not a justified reason to not use the classes. They are part of the Java class libraries and do their job correctly. Overall, they are useful and should be used when you require notifications in the application.




Applied Software Engineering Using Apache Jakarta Commons
Applied Software Engineering Using Apache Jakarta Commons (Charles River Media Computer Engineering)
ISBN: 1584502460
EAN: 2147483647
Year: 2002
Pages: 109

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net