Introduction
Sometimes it's difficult to see the woods for the trees. You know that using COM is
good, but how can you use it? How do you write an app that uses COM? How do you get from
IApe
and all the text book examples to something in the real world? This
article is intended to be a bit of a beginner's guide to a simple use of COM. Something
to try once you understand the basics and now want to write a real, working application.
By writing key pieces of code as COM objects and then allowing the user to configure
which objects to use at runtime using component categories you can write applications that
are easy to extend. I've tried to make this article a little different from most of the
'look at this cool way to do such and such with ATL' articles, they're fine if you're
desperately seeking the answers they give but they don't do much to help journeyman COM
programmers write their first real world applications...
You don't need to learn all of these bloated, complex, Microsoft interfaces inside out
to write good COM apps. You need to think about what you want, and use COM to achieve it.
The problem
Requirements change. It's a fact of life. You're just about to ship your application
when you suddenly need to support more new functionality. Without this functionality your
app is dead in the water as competitors have already leap-frogged your release
candidate... Unfortunately this kind of thing happens all the time. No matter how rapid,
or should that be rabid, your development process, requirements change at the least
opportune of moments. Being able to incorporate changes in requirements at a very late
stage in development seems to be the number one requirement these days.
Luckily COM can help with just this kind of problem. You have to put in a little extra
work up front, but if you do it can be well worthwhile. If aspects of key functionality
that have been identified as most likely to change are provided by simple in-proc COM
servers then you can simply plug in different functionality when business requirements
deem it necessary. What's more, since COM interfaces are immutable, if you got your
initial design right, the new functionality can be added without changing the rest of
the application. That localizes the changes behind the COM interface firewall - which
means less re-testing. If you do all of this using in-proc servers you hardly take a
performance hit at all, plus you can always rely on COM to allow you to remote pieces
of your application if necessary.
You can even take it one step further and allow the users of your application to
configure the components they require themselves - you could even publish a detailed spec
of your interfaces and let others develop replacement components for you!
Start with a clean design
Of course, to reap all of these rewards you have to put the work in at the start. You
need to analyze your problem domain and decide on what areas are most likely to fall
victim to requirements creep. Next you need to determine a clean way to break the risky
pieces of code out of the main app and present them as clean and extensible interfaces.
This takes practice. Sometimes, quite a lot of practice...
I find that a lot of these potential components fall out of the object model you build
up as you work on your design. Look for large chunks of functionality that communicate
with the rest of the application through clean interfaces. For example, if you have code
that drives pieces of external hardware but that don't require the complexity of a real
device driver, then these are a prime candidates for pulling out of the application and
making them into device drivers implemented as COM objects. If you have an area where
you're currently using a range of polymorphic objects through an abstract base class, then
chances are you could make each into a separate COM object and access them through their
common interface. Perhaps your application supports spell checking, image rendering,
compression, encryption? Perhaps you know that time is short and you aren't going to be
able to deliver 100% functionality by ship date. Turn the risky code into COM objects and
ship the stuff you can with release 1 and then ship the extra components when they're
done!
Don't stop at the first level of component that you find. Often the application could
simply serve as the glue that joins components together. In a credit card production
system I worked on the device drivers were COM objects which could present interfaces
which represented their functionality (encode a magnetic stripe, emboss a card, print on a
card, encode a chip card). The type of card was also a COM object (standard magnetic stripe
credit card, proprietary format magnetic stripe, chip card, etc). Both card and device
presented fixed interfaces to the application, but the device could present any interface
it liked to the card. It was up to the card to see if the device that the application was
trying to partner it with supported the interfaces that it required. The card type knew
how to make the card through the interfaces that were required, it also knew how to find
out if a device supported those interfaces and make intelligent decisions about downgrading
functionality on less able devices; a magnetic stripe card could be made on a machine that
could encode a stripe, or one that could print and encode, etc. A chip card needed chip card
functionality. The application itself didn't know anything about the interfaces used
between the card object and the device object. This allowed us to add new types of cards
needing new types of machine with new interfaces very late in the day without needing
to change the application at all... Do you suddenly have a requirement for a photo card, printed
on a new machine that you've never seen before? No problem: write a device driver for the
new machine, develop a new interface for the new picture printing functionality,
write a card that knows how to print pictures through the new interface and plug the whole
lot into the rest of the app without needing to change anything...
Let your objects talk to other objects and let the application introduce them and then
get out of the way!
Use component categories
It's all very well writing components but when you then tie everything together so
tightly that you might as well have permanently linked the components together at compile
time you lose much of the advantage of component based development. Beware binding
components together using hard coded GUIDs. Use a more flexible method (even if it's just
reading the GUID from a registry key...) Far better, use component categories. Allow your
user to select from any of the objects that implement the categories that your application
requires.
Using a simple wrapper around the standard COM category manager interfaces you can
write code that makes it easy to list all the objects in your required category. Put those
in a list box, let the user decide and store the decision in the registry. When you
provide improved functionality, ship a new object and once it's registered it will appear
in the list and allow for an easy functionality upgrade.
Persistent configuration
Complex objects that are user configurable should be able to persist themselves so that
the user doesn't have to continually repeat their configuration. You don't have to wade
through the documentation trying to work out which
IPersist
interface to use
though! Just make one up, one that works for your application and your situation. You
could have the object persist directly to the registry, but this could cause problems if
suddenly the application decides to allow the user to configure two of the object. To make
the object as flexible as possible it's probably best to persist to something simple, a
byte buffer that gets returned to the application but that is in a format understood only
by the object is fairly safe. The object can always save and restore itself to the buffer,
the app can persist the bytes wherever and however it wants. At the end of the day,
IPersistStream
is a pretty safe bet. It's easy to use from both component
and application ends and it gives the application the ability to change where it is storing
the resulting data stream to without affecting how the component writes out its configuration.
Build a framework for your components
COM's great for programmers, it's complex, takes ages to learn, and is always changing
- that means we get paid lots for being able to do it. Long may this continue, but
sometimes you have to take the pragmatic view.
Whilst it's all very well writing objects where you need to be fully conversant with Inside
OLE to be able to adjust them, it's far better to build a framework that allows people with
limited, or no, knowledge of COM to create new components for you. This is especially important
if you're the current COM Guru of your work place. Just think, do you REALLY want to be the
ONLY person capable of writing the all of the rest of the components for your system?
Where possible, write one or two of the particular class of component, then generalize
as much of the code as you can and factor it into boilerplate or library code that others
can use just by writing a normal C++ object. Then when the time comes to write the 10th
really boring serial card embossing device driver you can give it to anyone who knows C++.
Unfortunately I find that ATL and all of the wizards makes it far harder to do this kind
of thing. Unless of course, you write a wizard that generates 90% of your component for
you... The problem is the wizards don't encourage you to think about how you'll go about
writing the 10th driver that's almost exactly the same except it talks a slightly
different protocol down the wire. Cut and paste doesn't cut it.
Summing up...
Using COM doesn't have to be difficult. Lots of projects can benefit from just adding some
flexibility to the system by making some elements replaceable. You don't have to dive right
in and try and use every COM trick in the book to get some benefit. Simple uses have their
places, and help you to get used to working with the technology.
See the article on Len's
homepage for the latest updates.