The making of "The Simple Digital Watch" in (D)COM

Or... how did we port a C++ application to (D)Com(1)

 

Once upon a time there was a C++ application which was built after the OOA of a simple digital watch (SDW). The SDW was built up using layers and used a Mediator to connect the subsystems. The architecture is shown in figure 1.

Figure 1: The SDW Model

As you can see in figure 1, there are three subsystems, with three layers each, connected by a Mediator. Every layer, and the Mediator, is implemented as a C++ class. The Mediator relays calls from one system to the other. In the Source subsystem we find a Sensor at the bottom. This Sensor gives every second a pulse to the Converter. The Converter keeps track of the number of pulses it receives. After receiving 60 pulses the Converter notifies the Source to update the Watch. (of course this goes through the upper and lower layers) The Source gives an update signal to the ControlCentre. Although, it looks like it does so, but it really gives an update signal to the Mediator, who relays it to the ControlCentre. Now the ControlCentre updates the Watch, which sends a notify back to the ControlCentre. This notify contains the actual hours and minutes. The ControlCentre than relays the notify to the Interface (via the Mediator), which on his turn routes it via the Router to the Display. The Display than shows the actual time to the screen or the printer. This depends on the implementation. Building the system this way has the advantage that we can easily change the display type.

The Panel, in the ControlCentre’s lowest region, gives us the opportunity to manually change the current time. The Panel consists of two buttons: A and B. Pressing button A once freezes the Sensor (I presume it needs no more explaining that this signal travels via the Watch, ControlCentre, Mediator, Source, Converter and finally arriving at the Sensor!). Also it set the B button ready for changing the minutes. Pressing B now updates the number of minutes. Another press on A lets us change the hours. Finally a third time pushing A sets the Sensor back to pulsing mode again. Of course does every change in hours and minutes, caused by button B, immediately show on the display.

 

Okay, but how does COM come in?

The disadvantage of having a monolithic C++ application is that every little change in the application requires recompiling everything. So if we decide to change the Display, we have to recompile the Sensor class. COM gives us a solution to this problem. In COM everything is about interfaces. Once an interface is defined it may not be changed again because this would break existing clients which depend on the defined interface(2). How we implement an interface is up to the developer.

We now want to implement SDW using COM. Question is: How?

Well, figure 1 gives us the first, and almost final, idea. We can simply make a component out of every layer. This gives us 10 components (9 layers and the Mediator). The advantage that we now have is that when we make a change to the Display, we only have to change and recompile the Display component. The rest is left unharmed. But it is not al sunshine. In order to fulfill the COM criteria we most implement a classfactory and the IUnknown interface in every component. The classfactory is used to create the component and the IUnknown interface gives us a way to retrieve interface pointers to the interfaces the component supports.

We know from the analysis phase which interface every layer uses. We can use this to determine how to build the SDW model in COM. The interface that was determined in the OOA phase will be the main interface in every component. But, there is more! In order to let a component know which layers are above and/or below him we have defined an interface for this special task. This interface is given in IDL format in figure 2.

 

interface ISDWConnector : IUnknown
{
    HRESULT	SDWSetLowerConnection( [in] IUnknown * pUnk	);
    HRESULT	SDWSetUpperConnection( [in] IUnknown * pUnk	);
    HRESULT	SDWKillConnection();
};

Figure 2

As you see this interface is very rudimentary. It has three functions, two for creating the upper or lower connection and one for killing the connections. If we now create the components we can query the created component for its ISDWConnector interface and pass it the IUnknown pointer of its lower- or upper layer. Using this mechanism we create connections so the layers can communicate.

 

Now we know some theory but HOW did you implement it?

The first version of the Simple Digital Watch for COM we made was very simple. We just created components for every layer, and everything was implemented as an in-process component. This way we had 10 components, which could only be used on one machine, since everything was running in the same process. But it was a begin and it worked. But there were two things in this version we didn’t like at all.

One thing that we considered to be a problem was that the top layer components were created in the QueryInterface() method of the Mediator. The second problem was creating the other components in the top components QueryInterface()(3) method. So every time we asked for a specific interface we did a creation of components.

In the second version we still used QueryInterface() to create components, but now every component created its lower neighbor. The advantage of this approach was that the Source didn’t know anything about the Sensor, but was only aware of his direct upper and lower neighbors.

Still we weren’t satisfied with the solution we used in the second version, so we decided to make a third and final version. In this version we let the creation of components happen in the class factory and we decided to introduce three new components, called the Source-, ControlCentre- and Interfacecontainers. These containers are out-of-process components so now the system is ready for DCOM.

Figure 3

Figure 3

We now let a MFC Client (figure 3) create a Mediator component. In the Mediator’s classfactory we let the component read a configuration file which contains the 3 CLSID’s of the three containers. With this three CLSID’s the Mediator can then create the containers. At the creation time of a container the same process repeats itself, but then in the classfactory of the container. The container’s classfactory reads a configuration file containing the CLSID’s of the lower layers. Now these layers are created. Using this approach the whole system gets created by the simple creation of the Mediator, and still the Source is not aware of the Sensor and vice versa. The creation and the startup of the system will happen when we push the start button, and everything will be released when we press stop or when we eliminate the client window.

Figure 4

Figure 4

Upon creation the Panel component shows a window (figure 4) containing the A and B button. These buttons can be used to manually manipulate the time as described before. The window is created in the Panel component using the Win32 API-call CreateWindow(…). The mouseclicks on the buttons are handled in the WindowsProcedure(4) function belonging to this window.

Figure 5

Figure 5

Not only the Panel has a window to display. The Display component must show the current time. We also used a window for this purpose, and again it is a simple CreateWindow(…) API-call. This window is shown in figure 5. As you can see it simply shows the time.

Now we have a complete SDW implementation in COM according to the analysis we had of the system. There are some things to say about using COM for a system like this. The first thing that catches the eye is the enormous amount of source code. This is because every component has its own implementation of the IUnknown and every component also has a classfactory. Because we use nested classes we also have IUknown implementations in them (there are 2 nested classes per component, the ISDWConnector interface and the component specific interface).

The second thing is something more theoretical. Since we use so much different components we have a lot of overhead. Although we do use mostly in-process components (which are faster than out-of-process components, since in-process doesn’t need marshalling) the system must be slower than normal, but that is something we can’t measure with the current Pentium II computers and something as simple as the Simple Digital Watch.

Henk van Es
June 25, 1999
Datasim Education BV.

 

(1) From now on we will only refer to COM, since there is almost no difference between COM and DCOM on the outside.
(2) Existing interfaces that get extended are usually given the same name with a number added to it. E.g. IClassFactory and IClassFactory2
(3) QueryInterface(const IID & iid, (void **) ppv) is the method COM uses to ask a component for a supported interface
(4) The function were the window handle its messages from the messagequeue like WM_PAINT, WM_COMMAND etc.

[ Homepage | Articles]