CORBA hides the details of invoking objects on remote machines. With a little more refactoring, we can hide the details of CORBA as well.
Introduction
CORBA provides an industry standard mechanism for developing and deploying distributed objects. These objects can be implemented in a variety of programming languages and distributed over heterogeneous hardware. Well-designed CORBA systems are flexible and highly scalable. The benefits of CORBA, however, come with a high cost related to the complexity of the implementation and the difficulty of integrating with non-CORBA code.
When integrating CORBA services with C++ applications, the complexity arises from three main sources:
- mapping between CORBA data types and C++ data types
- exception handling
- switching between distributed and colocated architectures
In this article I demonstrate these problems, and a solution, using the example of a very simple message logging service.
The Complexity of CORBA with C++
A simple distributed message logging service will demonstrate the complexity of using CORBA with C++. The logging service is a program that runs on a server and is made accessible to any number of client programs via CORBAs distributed computing mechanism. CORBA makes it possible for a client to call the service whether that client is colocated [1] with the service, in a different process space, or even on a different machine. Clients can call the service to log events that have occurred and to provide other information, such as the name of the function where the event occurred and the severity level of the event. To log an event, a client calls one of the functions in the logging service interface. The logging service interface is a CORBA interface; this makes it possible for clients to call the service without knowing where the service is located.
Logging Service Interface
CORBA interfaces are described using IDL (Interface Definition Language). The logging service is defined by a single IDL module containing a single interface named Logger (Listing 1).
As can be seen from the IDL definition, the Logger interface consists of five methods:
- logMessage. The logMessage method logs a single string and associated severity level.
- logMessageAndMethod. The logMessageAndMethod method logs a class name, method name, message string, and associated severity level.
- logMessageAndFile. The logMessageAndFile method logs a file name, line number, message string, and associated severity level.
- messagesByLevel. The messagesByLevel method returns a CORBA sequence containing all messages with the specified level that have been logged.
- shutdown. The shutdown method terminates the Logger service and un-registers it from the CORBA Naming service.
The Logging Service Implementation
CORBA services are implemented by binding an implementation class written in a language such as C++ to an IDL-specified interface. This is done by compiling the IDL with an IDL-to-C++ compiler. The compiler creates what are known as skeleton files for the server side of the application. Skeleton files are C++ source files that represent the IDL-specified interface in terms of C++. These skeleton files are then compiled with C++ implementation files (using a C++ compiler) to build the service.
On the client side, the IDL-to-C++ compiler creates stub files. Stub files are C++ source files that present the service interface to client programs. They provide everything necessary for the client to call the service. These stub files are compiled along with the client source code to create the client programs.
IDL-to-C++ compilers are typically provided with (and are specific to) a particular CORBA ORB, or Object Request Broker. The ORB is a special module that handles all the communication between CORBA clients and services. An implementation of the ORB resides on both the client and server machines. To create the example implementation for this article, I used TIBCOs TIB/ObjectBus [2] ORB, but the code should work with any OMG-compliant ORB.
The Logger interface is implemented by a C++ class with the same name (Listing 2). The CORBAImplementation [3] parameterized base class encapsulates much of the boilerplate code required by CORBA services. The Logger class includes a run method in addition to the methods specified in the IDL declaration. This method is called to start the service.
The Logging Service Client
CORBA implementations provide a program called the Naming Service, which enables client programs to locate CORBA services by name. Once the Naming Service has been started, running a wrapper program (Listing 3) that performs the necessary ORB initialization and calls Loggers run method will enable the Logger service to be used by CORBA clients.
As the client code (Listings 4 and 5) demonstrates, the use of CORBA increases the complexity of applications and decreases understandability and maintainability:
- The client must use the CORBA Naming Service to acquire a reference to the Logger service.
- The client must use a number of CORBA data types that make the code difficult to read. Sequences (the equivalent of C++ arrays) suffer from a particularly inelegant interface.
- CORBAs exception handling mechanism results in code bloat that obfuscates the clients logic.
- CORBAs exception handling mechanism requires that developers explicitly check for an exception after each method invocation. These semantics are significantly different from C++ exception semantics and contribute to the difficulty in understanding the code.
- An annoyance to experienced C++ developers is CORBAs lack of method overloading.
Adding an Adapter
The Adapter pattern (as described in Design Patterns [4]) can eliminate much of CORBAs intrusiveness by encapsulating the CORBA-related complexity and presenting a "standard" C++ interface to the client application. This use of the Adapter pattern could also be considered to be an instance of the Proxy [5] pattern.
Adding an adapter is straightforward. The Logger class is renamed to LoggerImpl to better reflect its nature and a new Logger class (Listing 6) is created to implement the Adapter pattern. Figure 1 shows the relationship between the Logger and LoggerImpl classes.
Instances of the Logger class acquire a reference to the LoggerImpl during construction [6]. When a client calls a method of the Logger class, the Logger class forwards the call to the appropriate method of the LoggerImpl by reference. The Logger also translates CORBA exceptions to C++ exceptions; any exceptions that must be propagated to the client are raised using standard C++ exception semantics. The interface of the Logger class is indistinguishable from classes that are not dependent on CORBA; no CORBA-specific data types, exception handling details, or restrictions are exposed to clients of the Logger class.
Listing 7 shows that using the adapter is much easier than using the CORBA implementation directly. The data type issues, incompatible exception handling mechanisms, and lack of support for overloaded method names in CORBA have all been resolved by adding a level of indirection, thereby decoupling the client from the intricacies of CORBA and making the resulting system more easily maintainable.
Location Independence
The addition of a simple adapter increased the usability of the Logger service significantly. A few more small changes can provide even more flexibility to clients of the service.
During development of a CORBA system it is often more productive to implement and test the functionality of a service independently of the distribution mechanism. In addition, decisions regarding which services should be distributed and which should be colocated typically cannot be finalized until late in the development process. The distribution choices can even be revisited after a CORBA system goes into production, as usage patterns change or hardware and network enhancements suggest more optimal solutions.
Location independence is therefore a valuable feature of a distributable class. Ideally, clients of the class should not be aware that a class is distributed; instances of local and distributed classes should be treated identically. It should be possible to change the distribution mechanism of a class without impacting the client code.
This decoupling of interface and implementation can be accomplished using a variant of the Bridge [7] pattern (illustrated in Figure 2).
As described in Design Patterns, the Bridge allows different implementations to be provided for a single abstraction. The appropriate concrete implementation is selected at link time or run time and is called by the Abstraction class to provide the behavior of each method in Abstractions interface.
Implementation of the Location Bridge
The canonical implementation of the Bridge pattern involves two separate inheritance hierarchies with the interface class maintaining a reference to a concrete implementation class. This dual hierarchy approach is unnecessary to achieve the goal of location independence; instead of the full inheritance hierarchy assumed in Design Patterns, I use just two concrete implementation classes (see Figure 3). This simplification allows for the Abstraction and Implementor abstract base classes to be merged into a common abstract base class that defines the interface used by clients of the service. This common base class is named Logger [8].
To implement the location bridge, the Logger class from the adapter-only example is renamed to LoggerCORBAAdapter (Listing 8) and modified to inherit from the Logger interface.
LoggerImpl (Listing 9) is a C++ implementation of the logging functionality.
The LoggerImpl class from the adapter-only example is renamed to LoggerCORBAImpl and is modified to use LoggerImpl for its implementation (Listing 10). In addition to eliminating duplicate code, this design cleanly decouples the distribution mechanism provided by CORBA from the service behavior provided by LoggerImpl.
The correct subtype of Logger can be selected in any of several ways at either link time or run time. Some possibilities include initialization in main, the use of the Factory [9] pattern, and tricks with static initialization. The important issue is that clients of the service are insulated from the particular subtype choice and are dependent only on the Logger class.
In this example, the Logger base class requires its subtypes to implement a clone method and to register with the base class when the first instance is constructed. The Logger class can then be used without the need for a factory.
Benefits of the Location Bridge
This combination of the Bridge and Adapter patterns provides location independence and decouples the implementation of the service from the details of the distribution mechanism. Distribution alternatives can be selected at link time by specifying different libraries or even at run time based on command-line parameters or environment variables.
From the perspective of the production users of a system, the primary benefit of using this combination of patterns is flexibility. Repartitioning behavior between multiple processes is quite simple. From the perspective of the original developers and those responsible for maintaining the system, the primary benefit is that the implementation can be easily tested independently of the distribution mechanism.
Summary
It is worth noting that several existing ORBs do support colocation of CORBA services within the client process. The solution presented here has several advantages over such mechanisms:
- There is no CORBA overhead when the service is colocated; only the C++ implementation class is used.
- Client application code is insulated from CORBA data types.
- Client application code is insulated from CORBA exception handling mechanisms.
The use of the location bridge mechanism eliminates many of the common problems encountered when using CORBA in a C++ environment and provides some important benefits. The number of classes that must be implemented and maintained is higher than simply using "raw" CORBA, but the overall complexity of the service and the client applications is much lower.
Notes and References
[1] A colocated service is one that runs in the same process space as its clients. Naturally, a service may be colocated with some clients and distributed with respect to others.
[2] http://www.tibco.com/products/messaging/objectbus.
[3] A detailed discussion of this class is outside the scope of this article.
[4] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. Design Patterns, Elements of Reusable Object-Oriented Software (Addison-Wesley, 1995), pp. 139-150.
[6] The reference to the LoggerImpl does not, of course, have to be acquired in the constructor of Logger. It could, for example, be re-acquired for each method invocation.
[7] Design Patterns, pp. 151-161.
Patrick May is a consultant specializing in the design and development of large scale distributed object-oriented systems using C++, CORBA, relational and object databases, and whatever other tools are appropriate. He holds a degree from the Massachusetts Institute of Technology and is a U.S. citizen currently working in Luxembourg. His primary email address is pjm@spe.com.