by Mary Kirtland
COM+ is all about making it easier to write COM-based components and applications from any language. It does this by defining a standard set of types and making all components fully self-describing. This ensures that all COM+-compliant system services and components will be accessible to all COM+-aware languages and tools, in addition to simplifying deployment of components and applications that use them. COM+ makes the COM programming model more like the programming model of the language you use. You write classes in your favorite language. The tools you use and the COM+ runtime take care of turning these classes into COM components and applications.
To take full advantage of COM+—and to prepare your existing COM components to move forward to COM+—you need to understand the COM+ programming model. After examining the COM+ model, I’ll offer some guidelines for using the model to develop components.
One of the difficulties in writing about COM+ is that it really is language-neutral. COM+ doesn’t care what syntax is used to implement components—that’s up to the tool you use. All COM+ cares about is that the tool recognizes some syntax as representing a request to do some COM+ thing. The programming model concepts are the same, irrespective of implementation language, but the representation will differ. Throughout this article, I’ll use the syntax understood by the C++ compiler provided with the COM+ SDK to illustrate the concepts I’m discussing. Check your development tool’s documentation for the specific syntax it supports.
Note that at press time, neither COM+ nor the COM+ SDK was in alpha-release yet, so details may change.
Ask five different people what a component is and you’ll probably get five different answers. When I talk about a component, I mean a body of code that is capable of serving up objects of a particular type at runtime. Using this definition, a component would be implemented as a class in most modern object-based programming languages. Clients would just use the language features to create new objects, or instances of the class. For example, in a point-of-sale application, you might have CashierDisplay, CustomerDisplay, CashDrawer, BarcodeScanner, and CashierKeypad classes to represent elements of a register. The Register class initialization logic might look something like this:
Boolean Register::Initialize()
{
m_pCashDrawer = new CashDrawer();
m_pBarcodeScanner = new BarcodeScanner();
m_pCashierKeypad = new CashierKeypad();
· · ·
}
You get the idea.
COM is a system-level component model that cannot rely on language-specific features to create objects. Thus, COM components are a little more complex; in addition to a class, a component must contain a class factory to create new objects and, optionally, a type library to describe the classes and interfaces exposed by the component. So in addition to CashDrawer, you’d have CashDrawerClassFactory and so on.
COM+ attempts to make COM component development more like language-specific component development: regardless of the language or tool you are using, you just write a class. I’ll call a COM+ class a coclass to distinguish it from a language-specific class. In most cases, you will not need to write a class factory—the COM+ runtime will provide one for you. Nor will you need to create a type library. COM+-aware languages and tools use the COM+ runtime to generate metadata that fully describes your coclass. You can think of metadata as a type library on steroids.
So what’s all the fuss about a programming model? Write a coclass and you’re finished, right? Well, not quite. First, remember that COM+ components can be written in and used from any COM+-aware language or tool. That flexibility puts certain constraints on exactly how coclasses are implemented. You may not be able to use every last feature of your favorite language to implement a coclass. You may even find that some new features have been added to the language specifically for writing coclasses. So one thing to look at is just what it means to write a coclass, as opposed to any other kind of class you might write. Some tools, such as the COM+ SDK C++ compiler, will let you define both coclasses and classes. In other tools, all classes are automatically coclasses.
Second, as I discussed in my article, “Object-Oriented Software Development Made Simple with COM+ Runtime Services” (MSJ, November 1997), COM+ provides many standard services. Some of these services are used only by your tools, but others are accessible to your components. There is a service to handle concurrent access to instances of your class. Another service provides support for transactions. So another thing to look at is how you access COM+ services from your coclasses. The approach COM+ takes is called attributed-based or declarative programming and it may be a little different from what you’re used to.
Figure 1 shows the definition of a COM+ class, BarcodeScanner1. (The code samples are fragments designed to show COM+ features, not necessarily complete implementations.) You can see that this looks an awful lot like a regular old C++ class, and yet it is a completely functional (albeit somewhat boring) COM component implemented using COM+.
Figure 1: The COM+ BarcodeScanner1 Class
coclass BarcodeScanner1
{
public:
BarcodeScanner1();
~BarcodeScanner1();
private:
long m_lStatus;
};
BarcodeScanner1::BarcodeScanner1() : m_lStatus(SCANNER_OK)
{
// code to initialize scanner hardware
// (updates m_lStatus…)
· · ·
}
BarcodeScanner1::~BarcodeScanner1()
{
if (m_lStatus != SCANNER_DEAD)
{
// code to terminate scanner hardware
}
}
The keyword coclass indicates the class should be exposed as a COM+ class rather than as a C++ class. When this class is compiled, the C++ compiler will call the COM+ metadata emitter to generate metadata that describes the class. This metadata will be imported by clients that want to use BarcodeScanner1 to find out what the class’s capabilities are. I’ll talk about using components later, but take a quick peek at Figure 2 to get an idea of how it works.
Figure 2: The COM+ Register1 Class
#import "BarcodeScanner1"
coclass Register1
{
public:
Register1();
~Register1();
private:
transient BarcodeScanner1* m_pScanner;
};
// Constructor
Register1::Register1()
{
// To create a new COM+ object, just call new
m_pScanner = new BarcodeScanner1();
}
// Destructor
Register1::~Register1()
{
// No need to delete COM+ objects, just get rid of
// the reference
m_pScanner = NULL;
}
COM programmers may be wondering where the class identifier is—implementation class names are hardly guaranteed to be unique! In COM+, you rarely need to specify CLSIDs and other GUIDs explicitly. Your tools and COM+ may use GUIDS internally, but you can generally use fully qualified names in your sources. A fully qualified name includes namespace information (which I’ll discuss soon). Of course, if you need to specify a CLSID for the class you can use attributes, which are also discussed later.
A coclass defines both the public view of a type of object and its private implementation. Data members, or fields, properties, methods, and events make up a class definition. Fields and properties collectively describe an object’s state. Methods are the actions that may be performed on an object. Events are notifications that something interesting has happened. A class may be an event source that sends notifications, an event sink that receives notifications, or both. I’ll go into a little more detail about each of these in just a minute.
First, think about what kinds of data you might want to store in fields and properties, or pass around in methods and events. The language you use probably gives you a pretty rich set of types to work with. The language your clients use might support a different set of types. For a language-neutral object model such as COM, this is a problem. Everyone needs to agree on a common set of types, how they are stored in memory, and how they can be extended. Unfortunately, COM doesn’t do this. COM+ does.
Figure 3 lists the data types supported natively by COM+ and their sizes. In particular, COM+ natively supports all OLE Automation types, including VARIANTs and BSTRs. You can extend the known types by defining enums, structures, and classes based on known types.
Figure 3: Data Types Supported by COM+
TypeSize (bits)
Boolean | 8 |
BSTR | 32 |
Byte | 8 |
Char | 16 |
ClassRef (any class type) | 32 |
COM interface pointer | 32 |
Const (any native type) | |
Currency | 64 |
Date | 64 |
Double | 64 |
Float | 32 |
HRESULT | 32 |
Int | 32 |
Java array (any native type)
Long | 64 |
Pointer (any native type) | 32 |
Reference counted object reference | 32 |
Safearray | |
Short | 16 |
Sized array | 32 |
Unsigned char | 8 |
Unsigned int | 32 |
Unsigned long | 64 |
Unsigned short | 16 |
UUID | 128 |
Variant | 128 |
Void | 0 |
You should always use known data types (or types derived from those types) for class data, method parameters, and return values. Your tool may need to provide some sort of mapping between its native type definitions and those used by COM+. In addition, since COM+ classes are completely described by metadata, COM+ will often be able to coerce values you supply into the types it expects. Make sure you understand how much space is occupied by the data elements you use and what, if any, runtime coercion is happening to your data?you may be surprised!
An empty coclass isn’t very interesting. Let’s start filling in the details. First, your classes and objects may need to keep track of some data—they have state. Fields and properties collectively determine state.
Fields always occupy space in memory and may only be accessed from your class. COM+ can’t tell when fields have been read or modified. Properties, on the other hand, are implemented as accessor functions. This level of indirection to the object’s state lets you provide runtime code that executes whenever a property is read or written. In addition, nothing about the actual storage mechanism is implied by the existence of a property. This means that properties can be associated with interfaces. COM+ can detect when a property has been accessed—a key to interception. It can’t with fields.
Conceptually, field and property declarations look something like this:
[modifiers][attributes]type name;
I’ll discuss attributes a little later and I’ve already talked about types, so let’s look at modifiers. Modifiers fall into two categories: access modifiers and storage modifiers. The access modifiers are public, private, and protected. Fields are always private. Public properties can be accessed by anyone. Private properties can be accessed only within the class. Protected fields and properties can be accessed only by the class or classes derived from the class.
The storage modifiers of interest are static and transient. Static fields and properties belong to the class rather than each object. A simple example of a static field is a count of the number of existing instances of the class. This information can’t be held in each object; it must belong to the class as a whole. Transient fields are ones that should not be persisted with the object. For example, an elapsed time counter or a pointer to another object may not need to be saved with the rest of the object state.
I recommend that you reserve fields for private data used only by the class implementation, where you can control the values written to the fields. In addition, use the transient modifier to mark fields that should not be persisted. Use properties for all public state information or state which must be accessible to derived classes.
Behavior is implemented using methods. Your language may call them functions, procedures, or subroutines. Whatever they’re called, they do the work for your objects and classes. As with properties, COM+ can intercept method calls. Conceptually, method declarations look something like this:
[modifiers][attributes]return-type method-name(
[parameter-attributes]parameter-type parameter- name,…
[parameter-attributes]parameter-type parameter- name) throws [exceptions];
And somewhere, of course, you need to provide the actual implementation of the method.
One interesting thing about methods is that you can overload a method name within a class. In other words, you can provide multiple methods with the same name—as long as they can be distinguished by their parameter lists.
As with fields and properties, methods in COM+ may be decorated with access modifiers and storage modifiers. The access modifiers are the same: public, private, and protected. The storage modifiers are static, virtual, and abstract. Static methods are class methods rather than object methods. You don’t need an object to call a static method. Virtual methods are object methods that can be overridden in derived classes. I’ll discuss these more when I discuss inheritance later. Abstract methods are virtual methods with no implementation. In C++, abstract methods are also known as pure virtual methods and are designated using the syntax:
virtual return-type method-name(…) = 0;
You’ll see these again when I discuss interfaces.
COM+ also lets you specify the types of exceptions that may be thrown by a method. COM+ understands exceptions. Whenever boundaries between COM+ and non-COM+ execution environments are encountered, exceptions are translated to and from COM IErrorInfo, Win32® SEH, or C++ exceptions as needed. Exceptions thrown through intervening non-COM+ execution environments lose none of their semantics in translation.
COM+ recognizes two special types of methods: constructors and destructors. If you have developed COM components, you know these are new concepts for COM+. When COM objects are created, there is no guarantee about their state until an initialization function is called—and there’s no standard for what that initialization function should be! Most clients prefer that their objects be in a known state, so a variety of initialization protocols have cropped up over the years. This can make using objects unnecessarily complex. Constructors make sure that objects start out in the state you expect them to. When COM+ creates a new object, it will call the appropriate constructor based on the parameters passed to the creation call, immediately after allocating memory for the object. Likewise, if there is a destructor, COM+ will call it immediately before deallocating memory for the object.
One thing distinguishes COM+ classes from regular classes: the COM+ runtime has complete control over the creation and in-memory representation of all COM+ objects. Neither you nor your development tool knows anything about how an object is stored in memory. If your tool doesn’t distinguish between classes and coclasses, this doesn’t matter. But if you’re using a tool that does distinguish them, watch out!
For example, C++ developers routinely write code that makes use of a priori knowledge of the object layout in memory. But this won’t work for COM+ classes. You can’t do things like pointer arithmetic or taking the address of any part of a COM+ object. If your code makes assumptions about the layout of your object, it probably won’t work quite the way you expect it to. You have been warned!
So far I’ve talked exclusively about defining methods, properties, and fields on classes. This is a very different mindset from what COM programmers are used to. In COM, all communication between clients and objects occurs over methods defined on interfaces. The methods are implemented in classes, but defined on interfaces.
In fact, interfaces are still the recommended way to define the public behavior and state of your classes. An interface is just a definition of a set of related properties and methods with no implementation. Interfaces are an incredibly useful way for defining behavior: they define a semantic contract between a client and the implementor of the interface. You can implement interfaces in many otherwise unrelated classes, and clients can treat all those classes as if they were the same type of object—because all the client cares about is the interface. As with classes, I’ll use the term cointerface to refer specifically to a COM+ interface, just in case your development tool distinguishes these from some other type of interface.
Unless your coclasses are very simple with short life cycles, I recommend using cointerfaces to define their public behavior and state. Define your private and protected methods, properties, and fields directly on the coclass, but otherwise use cointerfaces.
Figure 4 shows the BarcodeScanner class rewritten using a cointerface, IScanner. As you can see, the C++ compiler provided with the COM+ SDK lets you define cointerfaces in C++ using the new keyword cointerface. You can also, if you really want to, use IDL to define cointerfaces, just like you do today for COM. You declare methods and properties within the cointerface just as you would for a class, except that everything must be public and abstract. For convenience, all methods declared within a C++ cointerface are public and abstract by default. As long as you use types understood by COM+, your cointerfaces should be accessible from all COM+-aware tools.
Figure 4: Barcode Scanner Using Cointerface Iscanner
// this is an interface definition in C++
// methods are pure and abstract by default, no
// need to clutter up the source code with modifiers.
cointerface IScanner
{
long Test();
long Reset();
};
// this is an event interface definition
cointerface IScanEvent
{
void NewScan(String strBarcode);
}
// BarcodeScanner class, now using interfaces, events,
// and attributes
coclass BarcodeScanner :
implements IScanner,
fires IScanEvent
{
attributes:
threading="rental";
public:
BarcodeScanner();
~BarcodeScanner();
long Test();
long Reset();
private:
long m_lStatus;
void CaptureBarcodes();
};
BarcodeScanner::BarcodeScanner() : m_lStatus(SCANNER_OK)
{
· · ·
CaptureBarcodes();
}
· · ·
void BarcodeScanner::CaptureBarcodes()
{
// of course, we'd really spin off a separate
// thread to watch for input, but this snippet
// will just show the code here…
for (;;)
{
String strScan;
// lots of work to read barcode into strScan
· · ·
// hey, we got one - fire an event
NewScan(strScan);
· · ·
}
}
// Just in case the barcode scanner doesn't work,
// the cashier can enter barcodes at her keypad.
coclass CashierKeypad :
fires IScanEvent
{
attributes:
threading="rental";
public:
CashierKeypad();
private:
void MonitorKeypad();
void OnCaptureBarcode();
};
· · ·
// assume the MonitorKeypad method sits in a loop that
// detects keypad commands and calls this method when
// the "Enter Barcode" button is pressed.
void CashierKeypad::OnCaptureBarcode()
{
String strScan;
// lots of work to read keypad into strScan
· · ·
// hey, we got one - fire an event
NewScan(strScan);
· · ·
}
As in COM, cointerfaces in COM+ are immutable. Once published, a cointerface cannot change (unless versioned, as described later). Because it defines no implementation, a cointerface cannot be instantiated, source or sink events, or contain any private methods. COM+ lets cointerfaces contain static data members, but no per-object data. COM+ cointerfaces support the multiple inheritance of other cointerfaces.
As with coclasses, COM+ interfaces are typically identified by using a fully qualified name instead of a GUID. Your tools and COM+ will generate interface IDs (IIDs) behind the scenes, where necessary.
Finally, you can source or sink events in your coclasses. Events are notifications that something interesting has happened. COM+ supports three types of events: interface-based, method-based, and persistent. Interface-based events are like the connection point mechanism used by ActiveX™ controls today. Sinks implement interfaces to catch events. Sources fire events by calling methods on those interfaces. The disadvantage of this mechanism is that even if you only want to handle one kind of event, you have to implement the entire interface it is part of. So COM+ introduces method-based events. These are exactly like interface-based events, except that the methods are defined directly, not as part of an interface. Interface and method-based events are examples of tightly coupled events; the source and sink know exactly what types of events will happen.
Persistent events are a little different—the source and sink don’t know much about each other. The source publishes information about events that can happen, and the sinks can subscribe to them. The source and sink don’t even have to be running at the same time. The programming model for this type of event will probably be a little different from that for tightly coupled events, so the remainder of my discussion will focus on tightly coupled events.
Figure 4 shows two coclasses that source, or fire, events. With the C++ compiler that comes with the COM+ SDK, you use the keyword fires to indicate that a class sources an event. Firing an event just looks like a method call. COM+ takes care of sending the event to all the sinks which are connected to it.
Figure 5 shows the Register class, ready to accept events from the input devices. To sink an event you implement a method, then point the source method at your sink method. In the example, the sink method is OnNewScan, which corresponds to the source method IScanEvent:: NewScan. The sink is hooked up to the sources in the Register class constructor.
Figure 5: The COM+ Register Class
#import "BarcodeScanner"
#import "CashierKeypad"
#import "IScanEvent"
coclass Register : implements IScanEvent
{
public:
Register();
~Register();
// IScanEvent
void OnNewScan(String strScan);
private:
transient BarcodeScanner* m_pScanner;
transient CashierKeypad* m_pKeypad;
};
// Constructor
Register::Register()
{
// To create a new COM+ object, just call new
m_pScanner = new BarcodeScanner();
m_pKeypad = new CashierKeypad();
// Hook up events. Note that both sources route to the
// same function. We don't care where the barcode scan
// comes from, as long as we get one
m_pScanner->NewScan = OnNewScan;
m_pKeypad->NewScan = OnNewScan;
}
// Destructor
Register::~Register()
{
// No need to delete COM+ objects, just get rid of
// the references
m_pScanner = NULL;
m_pKeypad = NULL;
}
// Handle new barcode scan event
void Register::OnNewScan(String newScan)
{
// do a bunch of work to look up barcode in inventory
// and create a new sales line item
}
At this point, I’ve covered all the kinds of things you can include in a class. You will also discover relationships between classes. One important relationship is the inheritance relationship used to construct class hierarchies. COM+ lets you use two types of inheritance: interface inheritance and implementation inheritance.
In interface inheritance, a cointerface can inherit from one or more existing cointerfaces. You then implement the cointerface in a coclass. In the C++ compiler provided with the COM+ SDK, you use the keyword implements to indicate that a coclass implements one or more cointerfaces. If you want to create objects of that class, you must provide implementations of all methods and properties defined by the cointerfaces and any cointerfaces they inherit.
Implementation inheritance is a little different. In this relationship, a coclass inherits both interface and implementation from another coclass. COM+ will let you use single implementation inheritance within a process. COM+ does not permit multiple inheritance of coclasses. Furthermore, COM+ classes may only inherit from other COM+ classes, and non-COM+ classes may not inherit from COM+ classes. (All Java classes are COM+ classes.)
Implementation inheritance is a new concept for COM. Although COM developers frequently use implementation inheritance within their component implementations, inheritance of an existing component has not been possible. Implementation inheritance has its place, however. It’s particularly useful for defining frameworks of standard, higher-level behavior. For example, you could provide a standard implementation of the interfaces used by ActiveX controls, BasicActiveXControl, and provide virtual methods at extensibility points—for example, Draw or Print. To create a particular control, say FancyButton, you would derive a new coclass from BasicActiveXControl and override the virtual methods.
Remember virtual methods? I mentioned them briefly when I discussed class methods. Basically, when you define a virtual method you’re saying that you may provide an implementation of the method, but derived classes can come along and override your implementation. The derived class implementation must have the same return type and parameter list to override the base class behavior. Now, when you have a reference to an object of the base class type, the actual object may be one of the derived types. When a virtual method is called through the reference, the derived class implementation will be used if there is one. Here’s an example:
BasicActiveXControl* pControl = new FancyButton();
pControl->Draw(); // calls FancyButton::Draw
// since FancyButton overrides Draw
One last thing about writing components. Remember those attributes I kept passing over? Now it’s time to talk about them.
A key concept for COM+ is interception. Whenever a method or property call is seen by your compiler or interpreter, instead of generating a direct call to the class code, the tool generates a call to the COM+ object services. This lets COM+ manage all method invocations and property accesses between clients and components. Each time, externally added services may execute. These are called interceptors. For example, a performance monitoring service could log the number of calls to each method and the time required to process those calls. A security service could determine whether the client was authorized to call a particular function.
The way you configure these services is by specifying attributes on your coclasses, cointerfaces, and their members. COM+ also recognizes some attributes directly (no interception involved). Basically, the idea is that instead of writing a bunch of code to initialize a system service, when you know all the values you want to pass to that service at development or deployment time, you just declare some attributes in your code. (This is why this technique is also called declarative programming.)
For a change of pace, Figure 6 uses the familiar bank example to illustrate how attribute-based programming looks. In the C++ compiler that comes with the COM+ SDK, attributes normally appear as attribute-value pairs within square brackets. Class-wide attributes are a little different, using the attributes keyword.
Figure 6: Attribute-based Programming
cointerface IBank
{
double DebitCredit([in]long lAccount,
[in]double dAmount);
}
coclass Bank : implements IBank
{
attributes:
transaction = "required";
data_source = "DSN=BankDatabase";
state = "stateless";
threading = "rental";
public:
DataSource m_dsBank;
[ source=m_dsBank, column="balance" ]
double m_dBalance;
double DebitCredit(long lAccount, double dAmount)
throws SQLException
{
m_dsBank.Source = "select * from Accounts where id="
+ ToString(lAccount);
m_dBalance = m_dBalance + dAmount;
return m_dBalance;
}
};
For example, you can give hints about how parameters are passed to methods using parameter attributes, as shown in the interface definition of DebitCredit. (COM programmers will find that many attributes closely resemble attributes you would see in an IDL file.) The attributes on the m_dBalance field show how to use the data-binding service to bind the field to a column of a SQL recordset, which is managed by the DataSource m_ dsBank. It is possible to specify attribute values at runtime, as shown in DebitCredit where the DataSource Source is calculated. The class attributes show how several other services can be initialized simply by specifying the attribute values.
What’s this good for? Well, for one thing, it gets all that nasty service initialization code out of the way of your business logic. For another thing, it lets you make decisions at the right time. If you know all the values required to access a service at development time, go ahead and set them. If the decision can’t be made until deployment time, the system administrator can set the attribute values then. And if you don’t know what the values will be until runtime, you can always write a couple of lines of code.
OK, everyone with me so far? You write COM+ components by writing coclasses. You should define the public interface of your coclasses using cointerfaces, and define the private implementation using methods, properties, and fields directly in the class. Use implementation inheritance to reuse standard implementations of functionality COM+ doesn’t provide for you. Use events to notify other objects when something interesting happens. Use class attributes to access COM+ services and configure third-party services. Piece-o-cake, right?
Most applications are built from many classes. To help manage classes, you can package them into groups of related classes. There are two ways of packaging: logical and physical. You create logical packages using namespaces. You create physical packages using modules.
A namespace is a named collection of classes and, optionally, other namespaces. The classes in a namespace may be spread across multiple modules, and a single module may contain classes from multiple namespaces. A class belongs to at most one namespace. If you do not specify a namespace, classes are added to the global namespace. Information about namespaces, their member classes, and the modules that implement those classes is maintained by the COM+ registration service. Namespaces are really, useful for reducing name collisions between classes. Just how many String classes do you think there are in the world? Put yours in a namespace and you’ll always be able to get to exactly the one you want.
A module is a single deployable unit of code, typically a DLL or EXE. (In last month’s article, I called these “packages.” To reduce confusion with Microsoft Transaction Server packages, the term “module” is now being used.) Modules may contain one or more classes. Normally, you put classes that are likely to be used together in a single module. In addition, you normally don’t need to write any module-wide code—you just link together a bunch of classes. If, however, you need to share information or perform some initialization at the module level, I’ve heard rumors that COM+ will define some standard functions you can implement, which COM+ will call at particular points in the module’s execution cycle—for example, when the module is loaded into a process and when it’s unloaded. These functions would be provided so you wouldn’t have to implement your own module entry point. I don’t have any definite information about what those functions are called. When I find out, I’ll let you know.
Once you’ve built modules containing your classes, it’s time for clients to use them. But first, clients need to know the classes are available for use. The classes must be installed and information about the classes written to a well-known location. COM+ provides a standard registration service that writes information about classes to a machine-specific database. COM+ figures out what information to write using the class metadata. You do not need to write any code to register your classes. This is quite different from COM, where components are required to provide a function that writes the correct values and keys into the system registry.
Administrative tools will use the COM+ registration service to install modules on a machine. Eventually, this service will be used by the Class Store and Component Download services as well and, over time, the system registry won’t be used to store component information anymore. For backward compatibility, administrative tools will give users the option of having the COM+ registration service write the necessary entries in the system registry as well as the COM+ registration database. The COM+ SDK provides a simple command-line administration tool called CORREG. When COM+ is released, a GUI tool called the COM Explorer will be provided.
It would be nice if you never had to update your components, but that’s rarely the case for successful components. So you do need to be concerned about versioning. There are two aspects of versioning: class versions and interface versions.
Hey, wait a minute! Didn’t I say that interfaces are immutable? Well, yes they are. But that doesn’t mean developers never need to change them: IPersistStreamInit, IAdviseSink2, IAdviseSink-Ex, IClassFactory2, IDispatchEx. I rest my case. Interface versioning is typically a developer issue. What you really want to be able to do is keep the old interface definition for existing clients, but create a new, improved interface definition for new clients—without creating some stupid new name for the new interface. COM+ gives you a way to do this. Remember that even though you write code using a string name for a cointerface, the actual identifier is an IID. So to version an interface, you give it a new IID and the old name. You do this using attributes on the interface definition.
// This is the original interface definition
[iid {7BE79088…}, rename "IScannerOld"]
cointerface IScanner
{
void Test();
void Reset();
}
// This is the new interface definition
[iid {BBA70095…}, rename "IScanner"]
cointerface IScanner2 : IScanner
{
void Beep(int beepType);
}
In this case, old clients will have code referring to IScanner that was bound to the original interface. New clients can choose to use the original interface, using the name IScannerOld in their code, or the new interface, using the name IScanner.
Class versioning, on the other hand, is often an administrative issue. Classes are also identified by GUIDs, and are associated with a module and version information. When classes are built, they are assigned unique class version identifiers based on a hash of the class metadata. If anything about the class changes, the version identifier will change! When a client is built, its metadata includes a version context and information about the classes it depends on—including the versions it was built with. By default, the loader will use the latest version of a class. If that doesn’t work, an administrator can specify an override, so that the client uses a particular version of one or more classes (typically, the versions the client was built with).
OK, so you’ve got some classes, built modules, installed the modules, and understand how to version your classes and interfaces, if necessary. How on earth do you use the classes? Fortunately, it’s really easy. A lot easier that it is in COM.
To get access to a class, you import metadata about the class into the client code. The most common way to import a class is by name or namespace. If a class is part of a namespace, the names-pace must be specified in order to import the class. For example, the following would import all classes in the POS.Register namespace and the Lookup class from the POS.Inventory namespace:
#import "POS.Register.*"
#import "POS.Inventory.Lookup"
The advantage of using this method is that you don’t need to remember what module implements the classes. When the compiler or interpreter calls the COM+ metadata importer to pull in information about the classes, the importer will use the COM+ registration service to determine which classes are needed and where they are implemented. Alternatively, you can import a specific file or CLSID. You can also indicate configuration information, such as a particular class version, in the import statement.
Once the class has been imported, you can create objects and call their methods and properties. You normally create COM+ objects using the same method you use to allocate native objects. In C++ you use the keyword new. This is shown in Figure 2 in the Register1 constructor. When you do this, your tool calls the COM+ memory management services to create an object of the specified type, call its constructor, and hand you back an object reference.
From the client perspective, this object reference is totally opaque to you. You don’t know anything about how the object is arranged in memory. It doesn’t really matter, though. You make method calls and access fields the same way you would for any other class. Your tool makes the correct calls to the COM+ runtime to call into the object. Figure 5 shows the Register constructor making a call to the BarcodeScanner::Test method. Properties are kind of interesting—you access the object using the property name, but your tool converts this into a call to the correct accessor method. When you’re finished with the reference, you just set it to NULL. COM+ takes care of cleaning up the object. COM programmers will be happy to see that reference counting is a thing of the past—COM+ takes care of it for you.
Another simplification for application developers is that all methods on the class can be accessed from the object reference. Unless there is a naming conflict, you don’t need to know what interface a method was defined on. If you do have a naming conflict, you just include the interface name in the method call to scope the method name. If you need to know whether an object implements a particular interface, you can ask whether the object is a particular type. The way this is exposed varies from language to language; in C++ it would look like a cast operation. (You could also call QueryInterface.)
It is important to point out that all COM+ objects are COM objects. The COM+ runtime takes care of providing implementations of IUnknown, IDispatch, error information interfaces, type information interfaces, class factories, connection points, and the standard DLL entry points, when needed. In most situations, the runtime implementations will work just fine. If those implementations don’t meet your needs, you can always provide your own.
In addition to the interfaces supported by COM+, there are many other COM interfaces already defined for things such as compound documents and ActiveX controls. COM+ does not directly address these interfaces, but you can easily implement and use COM interfaces in your COM+ components. In addition, application frameworks that provide standard implementations of these higher-level interfaces as inheritable coclasses are expected to appear.
The COM+ programming model is simple and straightforward. It maps naturally to the programming models of modern object-based programming languages. With COM+, writing and using components will be easier than it is with COM.
COM+ is about much more than writing and using components, however. COM+ is also about making it easier to write distributed applications by providing many of the services needed by these types of applications. In my next article, I’ll give you an overview of the services planned for the initial release of COM+ and talk about the programming model from the perspective of a distributed application developer. See you then!