| Rajinder Yadav - Windows/Linux C++ Development Tools & Resources :Design, Code, Test, Deploy |
Introduction
This article introduces the basic concepts of component-based development. It is a great starting point for those of you wanting to get into using Microsoft's Component Object Model (COM). Surprisingly enough COM is not limited to Microsoft or Windows, nor is it by any means a Microsoft Specific technology. In this article, you will soon see just how easy it is to implement a component using C++.
This article will guide you through the steps of turning a simple C++ class into a standalone component. It is assumed that the reader has a good understanding of C++ concepts.
Benefits of Componentware
Interface and Component
Simply stated a component is an object that provides its services through a set of interfaces, which themselves provide access to a collection of methods. An interface can be viewed as a contract, meaning that once a service is defined the conditions surrounding its use will never change.
NOTE: this definition of a component is not to be confused with the COM specification, which is a standard for component development.
Below is a diagram of a small component labeled "Cow". The diagram shows that the Cow component has two interfaces, IBase and IMouth. The IBase interface provides three services: QueryInterface, AddRef and Release, while the IMouth interface provides Talk and Eat. +----+
|Cow |----O IBase +-------------+ +----------------+
| |----O IMouth | <interface> | | <interface> |
+----+ | IMouth | | IBase |
+=============+ +================+
|Talk() | |QueryInterface()|
|Eat() | |AddRef() |
+-------------+ |Release() |
+----------------+
NOTE: By convention all interface names are prefixed with a capital "I"
In order to use a component, the application developer will require the following information:
We will see later how this gets implemented within a component.
Component is a Binary Image
The application developer using a component will not have access to its source
code. This should not matter, as the developer does not need to concern himself
as to how each interface is implemented. To a developer the component is
presented as a binary image that gets loaded during runtime.
Component as a Black Box
To the application developer a component is seen as a black box. The interface
is the only way for a developer to communicate with the component. The
developer cannot alter the state of the component directly or gain access to
its data. This has the benefit of data hiding, an interface in this sense
forces data and state integrity. The advantage of componentware is gained when
you consider the idea of binary compatibility. With this flexibility,
components become plug-able objects that can be used as interchangeable
building blocks.
Binary Compatibility
Components with the same interface signature can be interchanged with one
another. Now, the full power of componentware is realized when components
become dynamic loadable objects. In this framework, applications no longer need
to be recompiled. Given this ability it's easy to swap a slow spell checker
component with a faster one later on.
An entire application can be componentized and put together with "throwaway code" that glues each component together. This approach makes upgrading, extending, enhancing and customizing software a lot easier! Just think of the possibilities :-)
Improved Resource Management
Memory footprint and resource allocation can be reduced substantially with
componentware. In this model, the entire application no longer needs to get
loaded into memory, only what is required gets loaded. After all, why would you
want to load the entire word processor into memory? You only need the printer
component during a print request, likewise you don't need the html converter
unless you want to publish to the web, and forget email support if you're not
online.
Component Versioning
To ensure that successive releases of a component continue to run with previous
applications, the component developer must make certain to stick to the
following golden rule:
An Interface once released cannot change, that is the signature (methods) that define the interface must remain the same. However, the code behind the interface is free to change.
We will take a look at interface requirements in more details later, but for now let's look at some examples.
NOTE: how both components have the same signature for interface IAction
Interfaces are defined as pure abstract classes. What this mean is the interface is independent from the class that supports it. Thus many components can declare the same interface. Later we'll see how all components are required to support the base interface IBase.The following example illustrates how a component can support multiple interfaces.
In this example component SuperActor has two interfaces, IAction and IStunt. The code shows how each interface is used to gain access to the services. The following diagram shows how this is done through v-table lookup in C++.
V-Table Layout +----------+ |SuperActor| +==========+ |act() |<----- pIAction (pIAction = pSuper) |sing() | +----------+ |shop() |<----- pIStunt (pIStunt = pSuper) |phone() | +----------+
NOTE: how we #define "Interface" to be a struct in the last example
#define Interface structHaving an interface defined as a struct rather than a class makes all the methods public by default. This allows us to define interface IAction with two methods like this:
NOTE: The class must inherit's the interface publicly, so it is visible to the outside.
Interface Rule Revisited
In order for any idea to work there must be standards, and with components
there are no exceptions. Most of the rules you come across will have to do with
interfaces and lifetime management.
*** Interfaces Don't Change ***
Versioning
Once an interface is released, it cannot be changed. Instead when a component
requires an interface to change you must declare a new interface and add it to
the component. This way existing code will continue to work with the new
component and new code can take advantage of the latest interfaces.
Lifetime Management
To manage a component's lifetime, the component designers took advantage of a
technique called reference counting. The idea behind this is to keep track of
the usage count on a component. When a request is made for an interface, the
reference count is increased on the component. When the interface is no longer
needed, the interface is released and the reference count is reduced on the
component. When the reference count becomes zero the component is no longer
needed and is destroyed to free up memory and resources.
In order to support reference counting, we need to prevent the developer from being able to create and destroy the component directly. Since components are dynamic they should never be created on the stack, but component programming doesn't rule out this technique.
We don't want the developer to be able to use the new and delete operators to create and destroy a component. Allowing this would render reference counting useless, since one piece of code may be referencing an interface on a component that another piece of code just blew away with the delete operator!
1. We must declare the component's constructor and destructor to be private, this prevents the developer from creating the component on the heap with "new" and prematurely destroying it with "delete".
2. Next we need to add an interface to the component. We'll name the interface IMouth and give it two methods, Talk and Eat. For the Cow component to support the interface, the class definition must be updated to inherit it.
3. Next we need to add a reference count member to the component class.
4. Now we must provide a way to increase and decrease the reference count, so we can manage the components lifetime. To do this we need to declare a new interface called IBase.
Also our interface now must support the base services provided by IBase interface. To allow this, we must declare IBase to be a super class of our interface.
The IBase interface provides two methods AddRef() and Release(), which our component must now support to manage it's lifetime.
5. Now we need to declare a class factory in order to be able to create a component. If you noticed earlier, we declared class Factor to be a friend of component Cow. This allows the class factory to create an instance of a Cow component using the new operator.
The factory class had only one static method called Create that accepts two parameters. The first argument is the class id, it tells the factory what component we want created. The second argument is a void pointer to pointer that gets assigned a return interface of the created component.
6. Let's take a look at the code that creates an instance of the Cow components and uses referencing to manage its lifetime.
Putting it all together now!
Error Codes
One thing that you will notice with all interface methods is that they all
return an error code. The only exceptions are AddRef() and Release(), which
return the current reference count.
IBase Interface
All components must support the base interface IBase, it is the only way to
manage a component's lifetime.
If you've studies the code carefully, you will notice that the component has no way to support multiple interfaces! What's missing is a way to request for a particular interface from the component.
Requesting an Interface
A component with a single default interface is useless. What we need to add is
the logic to be able to request a component for a supported interface. To do
this we need to extend the IBase interface with one more method:
virtual long QueryInterface(const long iid, void** ppVoid) =0;
The first parameter is the interface id
The second parameter is a pointer that gets assigned a returned interface
Extending the component to support a second interface
1. Define a new interface
2. Update the component definition and support the new interface methods
3. Add support for QueryInterface
4. Update the class factory so we can request an interface during the component's creating process
What we've learned...