While designing software applications, you’ll encounter situations in which you need scalable communication among objects within and outside an application’s periphery. When certain states or conditions occur within an object, you need to notify other objects, and vice versa. This process is often called object wiring—tying objects together so that they can communicate with each other. In this article, we’ll create a pattern (for the .NET framework) for achieving object wiring in a scalable manner.
You can achieve this behavior by creating a program in which you hard-code an object notification matrix—a mapping between states within objects and a list of the objects to be notified on occurrence of those states. However, hard-coding an object notification matrix is a crude, nonscalable approach for achieving object wiring.
In day-to-day life, we use object notification quite often. I’ll use the network printer concept as an example for the rest of this article. A network printer maintains a queue of print requests (print tasks) from its clients and finishes those tasks according to criteria such as:
- · First come, first served
- · Priority of print tasks
In a situation where a network printer (a PrintTaskController object in our case) can’t print due to a mechanical failure or other problem, the printer should inform all PrintTask objects waiting in the queue. Next, those print tasks should inform the individual clients who requested those tasks. In our example, the PrintTaskController object must notify all of its pending PrintTask objects sequentially.
Synchronous and asynchronous notification
If we notify each task synchronously, PrintTaskController's responsiveness will depend upon PrintTask(s). For example, if one PrintTask hangs, then PrintTaskController will wait indefinitely and can't notify the remaining PrintTask(s). We loosely couple these objects to improve responsiveness of our framework. This means we adopt asynchronous event notification. During asynchronous event notification, the .NET Framework processes each event on a different thread, without affecting the responsiveness of the event-raising program. Figure A demonstrates the network printer flow.
|Network printer’s flow, and wiring between different objects|
There are two ways to implement generic object wiring behavior:
- · If an object must observe certain states of other objects, then it’s an Observer.
- · If an object must observe certain states within itself and notify objects registered with it, then it is Observable.
In this case, PrintTask objects are Observers because they want to observe certain happenings in the PrintTaskController object; the PrintTaskController object is Observable. In this framework, all Observer objects need to register themselves with Observable objects in order to receive notification. (Some people call Observable objects publishers because these objects actually publish events on certain conditions, and they call Observer objects subscribers because these objects subscribe to the events published by publisher.)
Fortunately, generic implementation of this behavior in various contexts is simplified, because .NET exposes certain keywords that are handy for implementing the behavior. The .NET framework provides two objects: delegate and event.
Delegate is a class that can hold references to a method. It is equivalent to a type-safe function pointer or CallBack. But, unlike other classes, a delegate class has a signature, and it can hold references only to methods that match its signature.
In our case, a delegate can act as a container for all registered Observer objects. Actually, Observer objects register their CallBack methods. The methods need to be called when Observable objects enter certain states. Every print task should register its callback method (like notifyClient()) with the PrintTaskController Object.
Event allows you to specify a delegate that will be called upon the occurrence of some event in your code. When we define a delegate as an event, the C# compiler adds certain operators with this event delegate type, such as += and -=. These operators are used to add or delete CallBack methods within Observable’s event object.
In this section and those that follow, we’ll describe the code for the Observer-Observable pattern. This pattern can be used to implement behavior where you need asynchronous wiring (event notification) between objects. We’re implementing this pattern in the EventDelegate namespace.
In the Observable class, we have defined a delegate with a signature and an event of a delegate type. NotifyTasks is a public method that is responsible for notifying all objects registered with this Observable object. It notifies objects by calling the OnObserved method. OnObserved is implemented as protected virtual so that classes that extend the Observable class can implement their own overridden OnObserved method.
For the asynchronous call of CallBack methods, we use the .NET Framework’s BeginInvoke method and pass an AsyncCallBack object and a function pointer to the CallBack method. (Refer to the .NET help for more information about the AsyncCallBack class and creation of the AsyncCallBack object.) Briefly, to create an AsyncCallBack object, you need to define a private method (AsyncReturn in our case) that takes a parameter of IAsyncResult type. This method will be called by .NET runtime when the CallBack method returns asynchronously. Listing A contains the code.
The Observer class in this pattern implements a public method whose signature must match the signature of the delegate, which will hold a reference to this CallBack method. Inside this method, we write logic that we want to call when the Observable object raises an event. Listing B contains the code.
To test the object wiring, we implement a Controller class. In this class, we first create a PrintTaskController object of the Observable class; then we create two PrintTask objects of the Observer class. After creating these objects, we register the observer method of the PrintTask(s) with PrintTaskController's event delegate. Now, in case of printer failure, the PrintTaskController should call registered CallBack methods asynchronously. For testing purposes, we have set printFailure to true. Listing C contains the code.
To run this application, use all the classes we’ve just discussed in the EventDelegate namespace and compile it in Visual Studio .NET or from the command line using the csc command.
The Observer-Observable behavior pattern described in this article is flexible, extensible, and robust for implementing responsive programs. You can apply it in multiple situation(s) where you need wiring between objects or event notifications. For example, similar patterns are used extensively in user interface programming, remote method invocation, and so on.