© 1998 Wilbert Bilderbeek, Harry Broeders and Alex van Rooijen.
One of the oldest and most well known design patterns is the "Observer" design pattern. (See Design Patterns: Elements of Reusable Object Oriented Software by E. Gamma, R. Helm, R. Johnson and J. Vlissides page 293.) This pattern makes it possible to design software modules that can be connected to each other by using a weak form of coupling. A program in which this pattern is applied is therefore easy to extend and maintain. The 68HC11 microcontroller simulator THRSim11 designed at the Rijswijk Institute of Technology (see http://www.hc11.demon.nl/thrsim11/thrsim11.htm) makes extensive use of the observer design pattern.
This article starts with a classic description of this pattern including a modern C++ implementation. Next we describe how we extended the classical implementation to make this pattern easier to apply. We will then show how we applied this extended observer pattern in the THRSim11 68HC11 simulator. Not only to weakly couple the user interface with the rest of the application (the most common use of the observer pattern), but also to implement breakpoints and to simulate electronic components for example logical AND gates. This heavy use of the observer pattern resulted in an application that consists of several components or modules which are loosely coupled together. Each of these software components consist of several subjects and observers which can be connected to the observers and subjects of other components. This makes the THRSim11 68HC11 simulator program very easy to extend for the maintainers of the program (that's us:-). We can, for example, connect a simulated AND gate to the pins of the simulated 68HC11 as easily as you can connect a real AND gate to the pins of a real 68HC11.
When the students of the Rijswijk Institute of Technology started to use the THRSim11 68HC11 simulator almost immediately the following question did arise: "Can I connect my own VisualBasic/C++/Delphi/FillInYourFavorite program to the virtual pins of the simulated 68HC11, so I can simulate my own microcontroller application completely?". In the last part of this article we show how we used Microsoft's COM (Component Object Model) framework to make it possible to use the observer pattern across different applications. This makes it very easy to connect your own simulated components with our THRSim11 68HC11 simulator. Visit the THRSim11 home page mentioned earlier to get some examples.
Click here if you are already familiar with the classic observer pattern.
The classic class diagram of the observer pattern is given in figure 1. To
discuss this pattern we use an example from our THRSim11 68HC11 microcontroller
simulation program. In the software module that simulates the 68HC11 CPU
several objects of the class Register
represent the 68HC11
programming registers. The contents of a Register
object can
be displayed in several ways in the GUI module. The contents of the CCR
(Condition Codes Register) for example can be displayed as a hexadecimal
byte in a register window and as eight individual bits in the status bar.
If the contents of a Register
object is changed by simulating
the execution of instructions, the different representations of this register
in the GUI should change as well.
The code that is needed to update the register window and the status bar
can not be included in the Register
class because we want to
keep this class independent of the GUI. A weak coupling between the GUI and
the Register
class can be realized by applying the observer
design pattern as follows. Each class in the CPU module which objects should
be "visible" in the GUI module (or in any other module) should be derived
from the abstract base class Subject
. See figure
1.
Figure 1. The Observer pattern.
Every member function in the class derived from Subject
that
changes the "visible" value must call the notify()
member function
which is defined in its base class Subject
. It will be clear
by now that the class Register
should be derived from
Subject
. This makes the class Register
completely
independent from the GUI (the most weakly coupling that is possible!).
Every class in the GUI that represents an object from within the simulated
68HC11 should be derived from the abstract base class Observer
.
Each object from the class derived from Observer
must contain
a reference to the object it represents. By using this reference the GUI
object can get or set the value of the object it represents. The coupling
between the GUI and the simulated 68HC11 is not as weak as vice versa
because the GUI must know how to preform this get and set. So the class
Registerwindow
used in the GUI should be derived from
Observer
. An object from the class RegisterWindow
must attach itself to a Register
object by calling this
Subject
's attach()
member function with itself
(this
) as the only parameter. The RegisterWindow
object must also connect itself to this Register
object setting
its concreteSubject
reference.
Each Subject
object contains a list of all Observer
objects that are connected to this Subject
. The class
Observer
has a pure virtual update()
member function.
This member function should be overridden in classes derived from
Observer
, such as RegisterWindow
. This member function
is, as we shall see shortly, called "automatically" when the value of a
Register
object changes. The RegisterWindow
object
can get the new value of the Register
object via its
concreteSubject
reference by calling Register
's
get()
member function (see figure 1).
This new value should then be displayed in the register window. Each change
of the value of a Register
object leads to a call to the
update()
member function of each RegisterWindow
that has attached itself to this Register
. This is accomplished
by the convention that every member function that changes the value of a
Register
object (for example set()
) calls the
notify()
member function in its base class Subject
.
This notify()
calls the pure virtual defined
update()
function for each Observer
in its list
of attached observers. This member function is overridden in the classes
that are derived from Observer
such as
RegisterWindow
. By applying the pattern we accomplish that the
Register
class has no knowledge about the number and the concrete
type of the Observer
objects in the GUI that represent (display)
the value of this Register
object.
Each ConcreteObserver
such as RegisterWindow
can
also control the value it represents by calling the Register
's
set()
member function via its concreteSubject
reference. The RegisterWindow
does not have to update its
representation after changing the value of a Register
object
because this will be done "automatically". The call to
Register::set()
leads via Subject::notify()
to
a call to the RegisterWindow::update()
member function which
calls Register::get()
to update its representation.
A straightforward implementation
of this observer pattern is given in listing
1. The list of Observer
s maintained in Subject
is implemented by using the standard C++ list
(linked list)
template as follows:
list<Observer*> observers;
The notify()
member function is implemented as follows:
void notify() { for_each(observers.begin(), observers.end(), mem_fun(&Observer::update)); }
The template function for_each()
is also included in the standard
C++ library. Stroustrup explains this algorithm on page 523 of
the third edition of
his C++ programming book as follows:
A key benefit of the standard library algorithms is that they save the programmer from writing explicit loops. Loops can be tedious and error-prone. The for_each() algorithm does nothing but eliminate an explicit loop. It simply calls its third argument for all objects in a sequence specified by the first two arguments.
Because for_each()
function needs a normal function as its third
argument we have to use the mem_fun()
template to transform
the member function Register::update()
to something
that can be called as a normal function. Stroustrup explains this template
on page 520 of his book as follows (slightly modified to fit our example):
The standard library mem_fun() template takes a pointer to a member function as its argument and produces something that can be called as a ordinary function which takes a pointer to the members class as a argument. The result of mem_fun(&Observer:update) takes a Observer* argument and returns whatever Observer::update() returns. The mem_fun() mechanism is important because it allows the standard algorithms (as for_each) to be used for containers of polymorphic objects (as list<Observer*>).
Because the member_fun()
template was added rather late (31
july 1997, see
http://www.sgi.com/Technology/STL/whatsnew.html)
in the STL (Standard Template Library, the original name for the library
that is now included in the C++ standard library) this function is
not supported by the Borland C++ compiler version 5.02 and
not supported correctly by the Microsoft Visual C++ compiler version 5.0.
See
http://www.sgi.com/Technology/STL/mem_fun_t.html
Note 1. The fixup we present (see
listing1) can be used without problems
in BC++5.02 and MVC++5.0.
When we evaluated the use of the classic implementation of the observer pattern (see figure 1). the following critical questions did arise:
ConcreteObserver
has the responsibility to call
attach()
and detach()
to attach itself to, or detach
itself from a Subject
. Can this responsibility that all
ConcreteObserver
s share be transferred to their common base
class (Observer
)?
ConcreteObserever
has to contain a reference to the
ConcreteSubject
it observes. Can this reference that all
ConcreteObserver
s define be defined ones in their common base
class (Observer
)?
ConcreteSubject
must be derived from
Subject. But a lot of these ConcreteSubject
s are just encapsulated
elementary types. Is it possible to automate the generation of these simple
ConcreteSubject
s?
ConcreteSubject
that is being observed by a
ConcreteObserver
is destroyed, the reference inside the
ConcreteObserver
becomes a dangling reference. Can this problem
be avoided?
ConcreteObserver
observes more than one
ConcreteSubject
the following problem arises. If the
ConcreteObserver
receives an update()
call it can
not determine the cause of this call. So the
ConcreteObserver
does not know which representation it should
update. The classic solution for this problem as presented by Gamma and Helm
(see: Observations on Observer, Dr Dobbs Source Book, sept/oct 1995) is to
send along a parameter with the update()
call. This can be the
this
pointer of the ConcreteSubject
that caused
this update()
call. But this leads to a switch
statement inside the ConcreteObserver::update()
member function
to determine the cause of the update()
call and take appropriate
action. This switch
statement switches on an alarm bell inside
our object oriented minds because it is more often than not a sign of improper
object oriented design. Is there another way to observe several
ConcreteSubject
s from within one ConcreteObserver
?
We extended the observer pattern as shown in figure 2 and now we can answer
yes to all of the above questions. As you can see we moved
the link from ConcreteObserver
to ConcreteSubject
to their base classes (from Observer
to Subject
).
This makes it possible to place the calls to attach()
and
detach()
in respectively the constructor and destructor of
Observer
. If a Subject
is destructed all attached
Observer
s are notified and they invalidate their link. This
avoids dangling references. (This is not shown in figure 2 but can be found
in listing 2.) A
ConcreteObserver
can determine the Subject
to which
it is connected to by calling the getPtrToSubject()
of its base
class Observer
. This member function throws an exception if
this Subject
is already destroyed so the
ConcreteObserver
can handle this situation properly.
Figure 2. The extended Observer pattern.
The template class Model<T>
can be used to create a simple
ConcreteSubject
that encapsulates a single variable of type
T
. So there is no need to define a new derived class anymore,
just instantiate this template. A very simple simulator module, for example,
containing some logical pins can be created as follows.
class Simulator { public: Model<bool> pin1, pin2, pin3; void run() { pin1.set(0); pin2.set(1); pin3.set(0); pin1.set(1); pin2.set(0); pin3.set(1); } };
If it bothers you that the data members are public remember than that their
purpose is to serve as connection points. Connection points of software modules
should be visible from outside just like the pins and connectors of hardware
modules. These pins can be driven by calling their void set(bool)
member function and can be read by calling their bool get()
member function. Observer
s can react "automatically" when someone
changes the value of a pin. If you need more functionality than is provided
by this simple Model<T>
template you can derive from this
class and add whatever functionality you want. Later on in this article you
will see that we did this to create a kind of Model
that can
be observed from within other applications.
The problem that arises when we want to observe more than one
Subject
from within a single ConcreteObserver
has
been avoided now because each ConcreteObserver
only contains
(in its base class) one link to a Subject
.
But this of cause does not solve the problem because we still might want
to construct a component that is observing more than one
Subject
. The solution is to let such a component contain several
(one for each Subject
to observe) special
ConcreteObserver
s. We designed the
CallOnWrite<T,C>
template to be used as such a special
ConcreteObserver
. A component class called C
that
observers one or more Model<T>
s (possible different types
of T
) can be constructed as follows.
In this class C
:
CallOnWrite<T,C>
must be defined
for each Model<T>
that must be observed.
CallOnWrite<T,C>
data member must be initialized
in the init-list of the constructor of C
with 3 arguments:
Model<T>
that must be observed.
C
that contains the member
function that must be called "automatically" when the
Model<T>
specified as first argument is changed (or is
being written to). The typical value used for this argument is
this
(the object containing this
CallOnWrite<T,C>
).
Model<T>
specified as first argument is being written
to.
CallOnWrite<T,C>
data member can be used as a pointer
to the Model<T>
it observes. The dereference operators
transform the Subject*
to an appropriate
Model<T>*
by means of a static_cast
. If you
try to observe a Model<bool>
with a
CallOnWrite<int,C>
you get a compiler error.
A component that is connected to pin1
and pin2
of an object of the Simulator
class we gave as an example earlier
and reports every value that is written to these pins is constructed as follows.
class Reporter { public: Reporter(Simulator& s): cow1(s.pin1, this, &Reporter::update1), cow2(s.pin2, this, &Reporter::update2) {} private: void update1() { cout<<"Pin 1 is made "<<cow1->get()<<endl; } void update2() { cout<<"Pin 2 is made "<<cow2->get()<<endl; } CallOnWrite<bool, Reporter> cow1, cow2; };
Listing 2 shows exactly the same application as was shown in listing 1 but in listing 2 the extended observer pattern is used. You can see that more code is shifted out of the application code into the observer framework. The use of this observer framework is easier than the use of the original observer pattern.
When the observer pattern becomes as easy to use as in our framework and
when you realise its full potential you start to see Observer
s
and Subject
s everywhere. In figure 3 some applications of the
observer pattern in our THRSim11 68HC11 simulation program are shown.
Figure 3. Using the Observer pattern in THRSim11.
The first two observers in this figure are edit controls that are contained
in a window. These controls display the value of the 68HC11's Program Counter
(PC). The first in decimal and the second in hexadecimal notation. These
edit controls can also be used to change the value of the PC. Observer 3
shows a breakpoint. This breakpoint component checks its stop condition on
every change of the object it observes (in this case the PC). When this stop
condition becomes true, the simulation of instructions stops. Because we
implemented breakpoints as a kind of Observer
we made it possible
to set a breakpoint on every simulator object that is visible in the GUI
(registers, pins, memory locations, etc).
Observer 4 in figure 3, the disassembler window, also observes the PC. If the PC changes the appropriate line in this window changes its color to green. So the user can see the instruction that is currently executed. This line is also scrolled into the window if needed. The disassembler window not only observes the PC but it also observes all memory locations that are visible (in disassembled form) in this window. This makes changes in the code that is being executed immediately visible. This is crucial for simulating self modifying code. When the memory contents is also shown in hexadecimal form in another window, the relationship between the assembler code (shown in the disassembler window) and the machine code (shown in the memory window) is becoming very clear. If a user changes the hexadecimal machine code in the memory window the assembler code in the disassembler window changes accordingly. Because the disassembler window has a build in line assembler it also work the other way around. If the user changes the assembler code in the disassembler window, the machine code shown in the memory window changes immediately.
Listing 3 shows the implementation and
use of the Breakpoint<Predicate>
template that can be
used to instantiate different kind of breakpoints. One of the so called
Predicate
function objects as defined in the C++ standard (see
Stroustrup 3 ed.
page 516) can be used as template argument and serves as stop condition for
the breakpoint.
When we implemented the simulation of the 68HC11's on board devices (timer,
serial ports, parallel ports, analog digital converter etc.) we also made
extensive use of this observer pattern. By using this pattern we constructed
independent components that are very easy to use and reuse.
Listing 4 shows the implementation and
use of two templates that can be used to instantiate every elementary component
you can think of that transforms one or two input values to one output value.
In fact these templates can transform any Predicate
or
Arithmetic
function object defined in the C++ standard (see
Stroustrup 3 ed.
page 516 and 517) or that you define yourself into a component that can be
used within the observer framework. This component observes its input values
and when one of them changes the function object that is given as a template
argument is used to calculate the new output value. The output is actually
only set when its value changes to prevent unnecessary updates. This also
eliminates the problem of circular everlasting updates that can occur when
outputs are propagated back to the inputs of the same component.
Figure 4. Using the Observer pattern to simulate logical components.
Figure 4 shows how to connect a simple simulator with some logical components.
pin1
and pin2
of the simulator are used to drive
an EXOR
gate (in this case constructed out of several elementary
gates). The output of this gate is connected to pin3
of the
simulator. All the code that is needed to create this configuration is shown
below.
typedef BinaryComponent<logical_and<bool> > And; typedef BinaryComponent<logical_or<bool> > Or; typedef UnaryComponent<logical_not<bool> > Not; void main() { Simulator s; Pin node1, node2, node3, node4; Not not1(s.pin1, node1); Not not2(s.pin2, node2); And and1(node1, s.pin2, node3); And and2(node2, s.pin1, node4); Or or1(node3, node4, s.pin3); s.run(); }
Listing 4 gives the implementations of the used templates and also shows a slightly more complicated application. As you can see now connecting a simulated logical or arithmetic gate to the simulated 68HC11 is far easier for us than connecting a real gate to a real 68HC11 is for you.
So we, the maintainers of the THRSim11 program, can easily maintain and extent our 68HC11 microcontroller simulation program. As time went by we extended the basic 68HC11 simulator with diverse external virtual components such as:
Figure 5. Using a NE555 timer for capacity measurements.
The users of the THRSim11 liked the extensions we programmed for them but they immediately added the remark that they would like it even better if they would be able to select from several virtual components and connect these components to the pins they want. Ultimately they would like to write their own extensions. This would give them a more beneficial product because they could completely simulate their own 68HC11 application.
This user demanded requirement was translated into a concrete question: "Is it possible to extend the simulator architecture so that separated software components can be coupled and decoupled without harming the observer design pattern architecture used within the simulator?" Furthermore, it must be possible to develop new software components separate (and independent) from the simulator. The user should be able to connect, in an arbitrary way, these new components to the I/O pins of the simulator, just like a real hardware component.
The connection between, for example a LED component and an output pin of the simulator is a perfect example of applying the observer design pattern. The LED software component is observing the output pin of the simulator and changes state if it gets a notify message from the output pin.
When the observer design pattern can be applied for connecting external software components to the simulator it will be very beneficial because:
For creating component software in Microsoft's Windows environment there is only one obvious solution: Microsoft's Component Object Model (COM).
COM is an important part of the Windows operating systems and delivers a base technology to dynamically couple server components to client applications. Furthermore, COM delivers a complete set of services to register and to search software components within a system. COM is the foundation for more familiar technologies like, OLE (Object Linking and Embedding), OLE automation and drag & drop. We present a short description of COM here. Click here if you are already familiar with COM.
Microsoft has introduced the Component Object Model (COM) which is defining a program language independent standard component model. Every modern design language works in one way of the other with objects: Delphi, Visual Basic, Java and C++. The objects are all specified differently in these languages. Directly (re)using an object, written in C++, in Delphi is therefore a difficult task.COM solves this problem, it defines a language independent view on what an object is, how it can be instantiated, which methods it supports etc. This makes it possible for designers to develop and produce language independent software components that can be reused in a standard manner in many applications.
The COM standard differentiates between a client application and a server application. The client application instantiates COM components and a server (DLL or application) produces COM components. A globally unique identifier (GUID) identifies every released COM component. This is a 128-bit unique value that must be generated and published by the designer (releaser) of a COM component. COM delivers a service for generating GUID's. The client application uses a GUID to instantiate a COM object. The first step in instanciating a COM object is to pass the GUID from the client application to the COM layer of the Windows operating system. The COM layer uses the registry to localize the server that can produce (instantiate) the particular COM object. The server will return a pointer to the interface (=set of functions) of a COM object to the COM layer which will return this pointer to the client application. This mechanism is illustrated in figure 6.
Figure 6. Instantiating a COM object by a COM client application.
The pointer returned by the COM layer to the client application is pointing to an interface structure wherefore the COM object delivers an implementation. The public member functions of the interface structure are always accessible in the same manner using this interface pointer. This is creating a language independent method for accessing COM objects, also called a binary interface. The interfaces of a COM component is being described using COM's interface definition language (IDL). Clients of a COM object can use this language to learn how to work with a particular COM object.
A common interface, every COM object must deliver an implementation for,
is the so-called IUnknown
interface. Notice that all COM interface
definitions start with an I
. The IUnknown
interface
consists of three functions: QueryInterface()
,
AddRef()
and Release()
.
Using the functions AddRef()
and Release()
, a reference
counter inside the COM object can respectively be incremented or decremented.
When the internal reference count of the COM object reaches zero, the COM
object destructs itself. This reference mechanism is built in because several
applications can use the same instanciation of a COM object at the same time.
When using the AddRef()
and Release()
functions
correctly, when copying or discarding a pointer to the object, no problem
will arise when one application destroys an object that is still being used
by an other application.
The function QueryInterface()
gives the client application the
ability to navigate between the different interfaces a COM object supports.
A client application can query a COM object to find out if it supporting
(or having an implementation for...) for example interface
ICustom
. The COM object will, when it has an implementation
for ICustom
, returns a pointer to the ICustom
interface, which the client can use to access the member functions of
ICustom
. Supporting several interfaces by a COM object implements
a kind of interface inheritance.
Using interfaces a client application
has the ability to access the functionality of a COM object. COM also defines
a technology known as connectable
objects. Using this technology a COM object is able to pass events (messages)
to a client application. To implement this a COM object must support four
different interfaces as described in the connectable objects specification.
One of these interfaces is the IConnectionPoint
interface. A
client application can determine, using this interface, which outgoing interface
the COM object supports. An outgoing interface is the opposite of a normal
COM interface because the interface is delivered by the client application.
The COM standard calls this kind of interfaces event sinks.
Using the IConnectionPoint
member functions
Advise()
and Unadvise()
a client application can
connect its event sink interface to a COM object. When the client object
connects to the COM object, the COM object is able to pass events to the
client application. The connectable object technology is illustrated in figure
7.
Figure 7. Connectable objects.
After a short study of the possibilities of existing COM technologies (like OLE automation) we concluded that it was not easy to apply these OLE techniques to the observer architecture of THRSim11. First of all the client application (simulator) would require mayor changes to support for example OLE automation. Secondly using a relatively slow OLE automation interface would result in great loss of performance. Performance is important in the simulator because every simulated clock cycle can cause a state change in a component if this component is connected to the CLK pin of the simulated 68HC11. Connecting to the CLK is necessary if the component, for example a function generator or a logic analyzer needs to run in synchronization with the simulator.
Fortunate it is possible to use COM's "connectable objects specification" to implement an observer design pattern a-like coupling between the simulator software and an external software component. Using this extended observer framework within the simulator and for developing external components, it will be possible to connect external server components run-time to the client application (the simulator) using the observer design pattern as a coupling mechanism.
A COM server component that can be used by the THRSim11 68HC11 simulator must support the following functionality:
This functionality is gathered in the COM interface IComponent
,
we specially defined to implement our framework. Of course this COM observer
framework must hide all COM details, query interfaces, reference counting
etc., for our C++ applications.
To describe the design and use of the COM observer framework we will use
the earlier described simple simulator example application. We will extend
this application with an independent designed and build COM server component,
an EXOR gate. The input pins of the EXOR gate will be connected to
pin1
and pin2
of the simulator. The output pin
of the EXOR gate will be connected to pin3
of the simulator.
Figure 8. An EXOR component connected to the simulator using COM.
You can see in figure 8 that the EXOR-component is observing
pin1
and pin2
of the simulator. The EXOR receives
a notify message when one of these pins is being driven by the simulator
application. The EXOR port output pin drives, by sending a set message,
pin3
of the simulator. Internally in the EXOR component the
two input pins will be observed and when they change state a new value will
be written to the output pin.
The first step to extend the simulator program with the EXOR component is to instantiate this COM component. The COM observer framework hides all details and we can simply create the COM object as follows:
DEFINE_GUID(CLSID_EXOR,0xAD6AFA60,0xD7A5,0x11D1,0xAB,0x71,0,0,0x1B,0x1C,0x56,0xEE); COMComponent exor(CLSID_EXOR);
The only thing the client application has to provide to the constructor of
the COMComponent
object is a GUID (Globally Unique IDentifier)
that is referencing to the EXOR component. Every COM component has this unique
identifier by which it can be registered in a Microsoft Windows environment.
The GUID is generated by the developer of the component and administered
in the Windows registry when the user of the component installs it. When
the COMComponent
constructor is being called for the first time
by the client application, the constructor initializes the COM library. You
can see the COMComponent
class as a wrapper for the COM
IComponent
interface.
When a COMComponent
object is destructed its destructor member
function releases the current COM component. When the
COMComponent
class determines that it is releasing the last
COM object this client application was using, it automatically uninstalls
the COM library. The above code fragment shows how easy it is to instantiate
external COM objects. It is then possible to get a window handle and integrating
the component into a client's graphical user interface. This will not be
shown in this article because all example programs use a console user interface.
The next step is to examine the COM object for models it possesses which can be observed or controlled by the client application. Here we encounter a problem that is a bottleneck for any object model that defines communication between language independent applications, the datatype differences. We can not simple define a structure datatype in a COM object that can be observed by an arbitrary client application. Maybe the client application is not written in a C like language and does not understand structures at all, for example Visual Basic. For this reason we have to use the standard primitive datatypes defined by COM, which can be used in all languages. Some examples of these datatypes are: boolean, byte, word, long etc.
These datatypes are used to couple the client application with the COM object. We defined a special COM object, called COM model to encapsulate a primitive datatype for each kind of the datatypes defined in COM to fit them into our observer framework. These COM models can be instantiated, using the COM layer, by a client application or, more likely, by a COM component. The COM component can instantiate as many COM models as needed and expose them to a client application. The COM observer framework defines a custom interface for accessing these COM models by a client application. This interface enables the client application to iterate through the available COM models where the client application can specify what type of model it is searching for (for example bit or byte models).
The COM observer framework hides all details for a client application that
wants to iterate through the COM models of a specific COM component. The
client application can simply use the member function
getNextBitModel()
of the COMComponent
class to
iterate through all bit models possessed by the COM object. When this member
function is called for the first time it returns a pointer to the first available
bit COM model. The second call will return the next model and so on, until
the last model has been reached resulting into an error code and resetting
the iterator function in the COMComponent
object.
A COM model itself has several similarities with a classical observer design
pattern model (subject). The interface of a COM model consists out of a
set()
and a get()
function. When the internal state
of a model changes it passes a notify message to all its observers. The problem
of independently passing notify messages to the observers of a COM model
is solved by using COM's connectable objects technique. The COM connectable
object specification is a technology that defines a standard way to connect
a client application to a COM object which enables the COM object to
pass messages (events) to the client application. To make use of connectable
objects the client application must define and implement a COM compatible
(binary) interface which can be used by the COM object to pass events.
As you have just seen, the function getNextBitModel()
provides
a pointer to a COM model. This pointer can be used by the client application
to read the value of the COM model by using the get()
member
function. The client application can also write to the COM model by using
the set()
member function. But the client application can not
observe the COM model because the COM model itself is not compatible with
our observer framework. To make it possible for the client application to
observe the COM model we create a specialized Model by inheriting from the
Model<T>
class. This new specialized class will be called
COMModel<T>
class. Figure 9 show the class hierarchy.
Figure 9. Class hierarchy of COMModel<T> and COM model.
This specialized COMModel<T>
class is a kind of wrapper
for the real external COM model. The following code fragment shows how the
client application gets a pointer to an external COM model and inserts this
into a COMModel<T>
object.
COMModel<IBoolModel> in1(exor.getNextBitModel()); COMModel<IBoolModel> in2(exor.getNextBitModel()); COMModel<IBoolModel> out(exor.getNextBitModel());
Listing 5 shows the constructor code
of the COMModel<T>
class. The first task of the constructor
is to call its parent constructor to initialize the value inside its
Model<T>
base class with the actual value of the COM model.
As the constructor code shows a special trick has been used to determine
the type of a COM model when only a pointer to its interface is available.
Without this trick it would be necessary to first call get()
on
a COMModel<T>
object to obtain a pointer to the real
COM model on which we must again call get()
to obtain the value
of the COM model. The COM model contains a
typedef NativeType
which defines the type encapsulated
in the COM model. This makes it possible to immediately return the value
of the data member encapsulated in the COM model when get()
is called on a COMModel<T>
object. The following IDL
definition of an IBoolModel
(bit model) interface shows that
the native type of an IBoolModel
COM object is
bool
.
DECLARE_INTERFACE_(IBoolModel, IUnknown) { typedef bool NativeType; STDMETHOD_(bool, get) (THIS) PURE; STDMETHOD(set) (THIS_ BOOL) PURE; }
The next step in the constructor code for the
COMModel<T>
object is to store the pointer to the real
COM model. Thereafter the constructor creates a ModelNotifySink
object. When constructing this sink object it passes a callback pointer that
the sink can use to pass events to the COMModel<T>
object.
An external COM model object can send events to the client application by
using this ModelNotifySink
object, which IDL interface definition
is as follows:
DECLARE_INTERFACE_(IModelNotifySink, IUnknown) { STDMETHOD(notifyUpdate) (THIS) PURE; // State of COMModel has changed STDMETHOD(notifyDestruct) (THIS) PURE; // COMModel is deleted (released) }
The IModelNotifySink
interface consists of two member functions.
The member function notifyUpdate()
which must be used by a COM
model to notify the client application when its internal state has changed.
The member function notifyDestruct()
which must be called by
a COM model when its internal reference counter reaches zero leading to a
self-destruct (its observers must be informed about this).
The ModelNotifySink
object passes these events directly to the
COMModel<T>
object by calling its member functions
COMNotifyUpdate()
respectively COMNotifyDestruct()
.
The COMNotifyUpdate()
member function calls the inherited member
function notify()
so that all observers, observing this COM
model are notified of an internal state change. The sink object will call
the function COMNotifyDestruct()
when an external COM model
is going to destruct itself. The COMNotifyDestruct()
function
will call the destructor of the COMModel<T>
class, this
will automatically detach the COM model from all its observers. The observers
can react on this if needed. When an observer tries to use the
get()
or set()
member function after the
COMModel<T>
object has detached itself an exception is
thrown. The destructor of the COMModel<T>
object will
furthermore use the unadvise()
member function of the COM
model to break the connection between the external COM model and the
COMModel<T>
object.
The connection between COMModel<T>
and the external COM
model is made in the constructor of the COMModel<T>
object
by calling the advise()
member function on the COM model. The
constructor code of COMModel<T>
(see
listing 5) shows the polymorphic features
of COM. By using the standard interface IUnknown
and its member
function QueryInterface()
it is possible for a client application
to navigate from the IUnknown
interface to the
ICOMModel
interface. This, custom defined,
ICOMModel
interface must be implemented and supported by all
primitive COM model types, like IBoolModel
,
IByteModel
, IWordModel
etc. Using the
ICOMModel
interface it is possible to retrieve the name of a
specific COM model (for example: "input pin IN1") and to use specific connectable
objects interfaces.
But a user of the COM observer framework does not have to bother with all
these details and can simply use the COMModel<T>
object
to observe, set and get the value of the external COM model.
Listing 6 finally implements the
configuration that was shown in figure 8. This is exactly the same application
as was shown in listing 4, but now the
EXOR gate is an external COM component which is developed completely independent
from the client application. The pins of the simulator and the pins of the
external EXOR are connected to each other by means of a Buffer
component. These buffers are used to connect the internal and external models
together. In this simple example the client application is aware of the existence
of the external EXOR server component. The client application is furthermore
responsible for making the appropriate connections.
In our THRSim11 68HC11 simulator application we want to give the users of
our simulator the responsibility to create, install, and connect their own
components. This is accomplished as follows. On startup the THRSim simulator
searches the Windows registry for COM server components that can be connected
to the simulator or, in other words, that support the IComponent
interface. The simulator determines the names and GUID's of these
components and fills a specific part of the simulator menu bar with these
names. If the user selects a component name from this menu the simulator
instantiates the specified component using the GUID found in the registry.
The simulator opens a new window in which the component can draw its graphical
representation. At this moment the component is instantiated but is not connected
to the simulator. The user can select the components window and choose the
connect option from a pull up menu (right click the components window)
or from the menu bar. This causes the simulator to examine which COM models
this COM server possesses. A connect dialog box is setup which displays the
names and types of all found COM models within this COM server on one site
and the appropriate simulator models (subjects) on the other side. The user
can make the connections he or she likes by using this dialog box. A COM
BoolModel
can, for example, be connected to every
Model<bool>
that exists in the simulator (all pins of
the 68HC11). A COM ByteModel
can, as another example, be connected
to every Model<Byte>
that exists in the simulator (every
8 bit register of the 68HC11 and every memory location). It is also possible
to create new nodes to connect external components to each other using this
connect dialog box. This enables the users of the THRSim11 68HC11 simulator
to implement, install, and connect their own simulated components completely
independent from us, the designers of the THRSim11 program.
As demonstrated the COM observer framework makes it very easy to transparently integrate COM components into a client application that uses an observer design pattern architecture. Using the COM observer framework it almost looks like application borders disappear.