Allen is a programmer, educator, and OO-design consultant. He can be reached at http://www.holub.com or [email protected]
The first reference I saw to the idea of a component-based architecture was in the book Object-Oriented Programming: An Evolutionary Approach (Addison-Wesley, 1991), by Brad Cox (one of the inventors of Objective-C). Cox called the components software ICs (as in "integrated circuits"), which is not a bad way to look at them. Look at a program as a circuit board, into which you plug various ICs that do the real work. As long as two ICs are pin compatible, you can unplug one and plug in another without affecting the main body of the program at all. If, for example, a program needed an editor, you could plug in an editor IC. If you didn't like the editor, you could pull it out and plug in another one with an identical interface without having to modify anything else.
One exciting aspect of this architecture is the possibility of a cottage industry in components. Monolithic applications built by monolithic corporations would go the way of the American Pilgrim (ten brownie points to anybody who knows what an American Pilgrim is), and be replaced by programs that glue together off-the-shelf components put together by two guys in a garage. To realize this model, though, there would have to be standards--you can't replace one editor with another unless they both have the same interface--and nobody seems to be screaming for standardized components, even the little guys who could benefit most.
Still, a component architecture is a great way to organize a program internally. It's quintessentially object oriented--each component is a black box with which you communicate over a well-defined interface. Change one box and none of the others are affected. Ideally, you can modify an existing component (fixing a bug, for example) and plug it into the framework without recompiling or linking the framework. The components could be distributed over a network and either executed remotely or downloaded to a client and cached for later use. You'd always have the most recent version of a component because the framework could compare the cached version with the original and download a newer version if the cached one was out of date. Well-designed components could be extended using derivation--if you didn't like the way a component worked, you could derive a new component from the original one, specifying only the desired changes in the derived class.
There are two competing ways to approach the component problem in today's world: Microsoft's COM/OLE (used with either C++ or Visual Basic) and Sun's Java. (I'm not putting OMG's CORBA/OpenDoc in this list because you can look at Java as a convenient way to encapsulate the CORBA model. You should certainly investigate the underlying CORBA stuff if you want language independence. Technically, OpenDoc is far better than OLE, but technical merit has never had much to do with acceptance in the marketplace.)
The OLE/COM Model
Microsoft has a propensity for pushing technology that the company itself doesn't use. MFC is one example--to my knowledge, the only Microsoft applications written using MFC are the Developer Studio and WordPad. OLE/COM is really in the same category. None of the Microsoft products are designed using a true component model. If they were, you would be able to plug your own editor module into Developer Studio or Word simply by creating an OLE/COM module with the correct interface. This isn't possible, of course. Given this half-hearted support for the architecture, it's not surprising that OLE is not particularly well designed. The basic idea of OLE/COM is to expose a C++ class's virtual-function table to the outside world. By making a complicated set of function calls, you can load a DLL that contains the member functions for that class, instantiate an instance of it, and get a pointer to the instance. Nothing earth shattering here. You can do most of this with a normal DLL. The main thing that OLE/COM adds is the ability to create an instance of the class when all you have is a numeric identifier (called a "globally unique identifier," or GUID) for the class. The system finds the correct DLL and gets it loaded for you. If you look at the C++ class as an interface to a component that's identified by this GUID, you can provide your own version of the interface simply by declaring a class with the correct members (in the correct order), and giving it the same id as the component you're trying to replace. Your class will then be created whenever the system asks for an object using that GUID.
That's the first big problem with OLE/COM. Since the client application is effectively accessing functions in a C++ class through a pointer, the client application will have to be recompiled if the class definition changes. You can't even add functions to the class definition without modifying any existing functions, because you can't guarantee that the extended class will be laid out correctly. So much for ease of use or maintenance. Moreover, the virtual-function table has to be laid out as if it were generated by Microsoft C++. So much for portability. Also, you can't implement an OLE/COM object in a language that can't simulate a C++ virtual-function table. So much for language independence. In short, you can use any implementation language you want, provided that it's C++. (Visual Basic, for example, doesn't let you access COM interfaces.) Additionally, you can't use inheritance to add functionality because objects are created using the "factory model" (see Design Patterns: Elements of Reusable Object-Oriented Software, by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Addison-Wesley, 1994). OLE/COM objects are created by calling a function that instantiates an object of a specific class. In other words, it's not polymorphic. When you ask for a particular class, you always get an object of exactly that class, not a derived-class object that implements the base class with modified functionality.
Microsoft tried to get around some of these problems by borrowing some features from CORBA, particularly the interface-definition language (IDL). Microsoft's "object-definition language" (ODL) is a poor second cousin to the CORBA IDL. You can use it to define a set of language-independent interfaces to a true component. You run the interface definition through a language-dependent compiler to create a set of functions for whatever language the client application is written in. These functions, when called by a client at run time, effectively look up their counterpart in the server in a dispatch table, and call the required server function. The architecture is the basis for the CORBA "remote procedure call" (RPC), and works across applications, not only those written in different languages, but those running on different hardware platforms on the same network as well. (The dispatch table is maintained on a network-wide basis by an application called the "name service.")
Microsoft has renamed this mechanism "OLE automation," but it's nothing but dumbed-down CORBA RPC. The only difference between RPC and OLE automation is that the function dispatch is done a bit more efficiently when the server is actually a DLL; in this case, an OLE/COM interface is used to get to the dispatch table, rather than messing with system-level network services. If the component is implemented as a stand-alone EXE file rather than a DLL, OLE automation is actually less efficient than straight RPC because you still have to talk to a DLL to get to the remote function via RPC. (The functions in the DLL effectively make RPC calls to the server.) The much-touted "distributed COM" (DCOM) interface that Microsoft is pushing right now is also just a layer around RPC. You call a function using an OLE/ COM interface, and that function calls a function in a server application on a different machine, using RPC. (Microsoft calls this process "marshaling.") I frankly don't see the benefit, here. Why not just make the RPC call and get it over with?
The main advantage of OLE automation over COM is that you don't have to recompile if the automation server changes internally (provided that the interface stays the same). Note that an MFC-based automation client does have to be recompiled if the server changes, however. You won't have to recompile a Visual Basic application, though. Mixed-language programming is also easier. Visual Basic has no problem accessing a C++ component via an exposed automation (rather than COM) interface. The main disadvantage of automation is slower access to the function. OLE automation solves none of the other OLE-related problems, however. Inheritance is still not supported, for example. Moreover, Microsoft uses a screwy (that is, not compliant with CORBA) implementation of the name service, which effectively makes it impossible to make an RPC call into a non-Microsoft (UNIX, for example) box without buying a Microsoft-compliant implementation of what should be a standard service. (You can do this, but it's a big-bucks item.) In other words, Microsoft has taken a perfectly good standard (CORBA), broken it, and then told us that we have to buy expensive programs that support the broken interface rather than use the free ones that come with all operating systems in the world except Microsoft operating systems.
The final problem is that the OLE/COM interfaces that Microsoft itself has defined (which we have to implement) are complex and poorly documented. Using MFC to implement an OLE app can probably save you work on the OLE side, but in the long run, using MFC will cost you because the program will be bloated. Sun has reimplemented its Java run-time libraries for the Microsoft/Intel platforms by tossing MFC and writing directly to the underlying Windows APIs. The resulting new version is half the size and at least twice as fast as the MFC version.
Java: A Better Way?
Fortunately, there's a way out of this morass--Java. Java is an object-oriented programming language developed at Sun Microsystems that takes a completely different approach to the component problem than OLE. Syntactically, Java is a lot like C++, so it's easy for C++ programmers to learn. It fixes many of C++'s problems at the cost of some flexibility at the design level. (Multiple inheritance is supported in a very limited way, for example.)
Java is a two-part language. A compiler translates Java source code to machine-independent "bytecode"--a sort of assembly language for an ideal (from a compiler-writer's perspective) computer called a "virtual machine" (VM). The byte-code is interpreted on the target platform by running it through a virtual-machine simulator. A Java application can run on any operating system--Windows, Windows 95, NT, the Macintosh, and all the UNIX platforms--literally without modification. All you need is a VM implementation on that platform (free versions of which are available for all the platforms just mentioned).
Unfortunately, interpreting the bytecode can be rather slow. Java bytecode, however, has a deliberate similarity to the intermediate languages most compilers use to talk to their optimizers. It's possible, then, to implement a VM as a compiler back end. Asymetrix has done just that with stunning success. The Asymetrix VM implementation (part of a larger package called "SuperCede") inputs Java bytecode, optimizes it up the wazoo, and then creates an EXE file that runs at C++ speeds. Think of it as delaying the final stage of compilation until the program is executed for the first time. SuperCede solves the "performance problem," at least on Intel platforms. (The VM implementation is available for free from Asymetrix at http://www.asymetrix.com/nettools/vm.) Sun is working on a similar optimizing virtual machine for its own operating systems and hardware.
Since Java is interpretive in nature, it can (and does) delay linkage until load time. This feature is what makes Java so useful in a component architecture. A component (even one that behaves like a control) is nothing but a Java class, the full definition of which isn't known until run time. The VM just reads in the class definition the first time you reference the class. There's no need for COM-style interfaces or OLE-automation-style dispatch tables.
You replace an existing class just by changing the definition. Adding functions, changing the order of functions in the definition, and the like is no problem. Recompilation of the client is never necessary. Moreover, polymorphism works just fine--existing code that's written in terms of base-class objects can use derived-class objects. This is impossible to do using OLE. In fact, Java renders OLE utterly irrelevant. It lets a programmer do the same things as OLE without any of the complexity.
OLE is a factor in the real world, however, and it can't be ignored. All of the Java vendors (Sun, Symantec, Borland, Asymetrix, and perhaps Microsoft) are integrating OLE support into Java so that an OLE object can be accessed from a Java program as if it were a native Java class, and a Java class can be exported as an OLE/COM object. The distributed-processing problem will be solved under the covers by mapping to CORBA (and OLE) models. The best part of this is that all of the OLE stuff will be done for us by the Java-compiler vendor. We can just write in Java and let the compiler worry about OLE.
Of course, you could do a lot of this with C++ if you had the resources of, say, a randomly chosen large company--Microsoft for instance. Imagine that OLE was a binary standard that was utterly irrelevant to a user of a high-level language. All that you would need to make a C++ class externally visible would be to declare it with an ole_
export keyword on the server side and an ole_import keyword on the client side. The compiler would do everything else for you. Moreover, imagine that all the features of C++, including multiple inheritance and polymorphism, were available to you to use in the exported object. Of course, Microsoft could have implemented OLE in this way if they had the will, but they didn't, and are not likely to admit their mistake. Java will implement OLE in this way.
Even though Microsoft is giving lip service to Java in the guise of its Jakarta and Visual J++ projects, I have yet to see any real commitment. The company is pushing ActiveX, which is nothing but a renamed OCX control--a DLL that uses OLE automation to talk to the outside world. Microsoft wants us to write applications in Visual Basic and use OLE to talk to external components. Microsoft also wants us to download ActiveX controls as a side effect of a web-page download and then execute the control on the client machine.
Frankly, I don't see the point. Straight Java does everything Visual Basic and ActiveX do, but better. Rather than create an external component with a complex OLE-automation interface, you can just implement the component in Java and use the class definition as a native class. In fact, many OCX-like controls (grids, progress bars, and the like) are already implemented in Java. I'll review some of these in a future column. I expect that Java applications will be able to use existing OLE controls as if they were native Java classes within a few months. Unlike Visual Basic, Java is a true object-oriented language that supports inheritance and so forth. I haven't even discussed some of the other important features of Java. It's secure in a network environment, for example, and it solves the memory management problems endemic in C++ and Visual Basic programs. So my advice, for what it's worth, is wake up and smell the coffee.