David George
Microsoft Corporation
October 2, 1995
David George is a Principle Consultant for Microsoft Consulting Services in Tampa, Florida.
Click to open or copy the files in the HOTEL sample application for this technical article.
This article compares the design factors and the mechanics involved in bringing about component-based designs in both the Visual Basic® and the Visual C++™ development environments. The article also shows how OLE is a rather compelling technology for creating complex applications.
Until the introduction of Visual Basic® 4.0, developing component-oriented business objects along the lines discussed in the Microsoft® Solutions Framework was primarily the domain of those individuals who had mastered Visual C++™. Although these components could be used within Visual Basic 3.0, there were some rather significant restrictions. With the addition of key features such as public classes along with Visual Basic Applications Edition syntax, the final hurdles have been removed to allow Visual Basic programmers to participate as full-fledged members of the OLE component development community, both for using and developing OLE-based Business Service components. Given this situation, it's important to compare both the design factors and the mechanics involved in bringing about modular, component-based designs in both environments, as well as point out the factors along the way that make OLE a rather compelling technology for complex applications.
It is assumed, for the sake of this discussion, that the reader is familiar with the concepts introduced within the Microsoft Solutions Framework, in particular the idea of the three-service Application Model and the process of Conceptual, Logical, and Physical Design. Although it is not absolutely necessary to understand these concepts in order to gain benefit from this discussion, they can significantly add to the reader's ability to internalize some of key points being made.
In my encounters with programmers, it is interesting to note the type of answers I get to this question. Generally, most developers point to the fact that Microsoft has made it abundantly clear that OLE is the future, and that if they don't focus on it they will likely not have much of a future in programming within the Windows environment. The reason I find this interesting is that most people don't normally use something unless they perceive they will be able to gain some key benefit that they would not otherwise have. Granted, if you're a programmer, having the skills to be employed can be considered to be beneficial, but I would like to propose the novel, and possibly contentious thought, that perhaps the driving reasons to design and program to the OLE model is that it can significantly simplify the process of design to development and could actually provide the capability to employ some new and unique design models for robust interactions between User Services, Business Services, and Data Services.
First, it is very important to understand the similarities and differences between a component technology like OLE, and object technologies such as Smalltalk or C++. In terms of differences, all technologies are flavored by the market for which they are targeted. Object technologies are primarily driven toward increasing programmer productivity through reuse of code. For example, once a class has been established within C++, it can ideally be employed and extended in functionality for use in other similar development projects. Notice I said ideally, because in reality there are a number of impediments to reuse when it comes to a broad group of programmers reusing everyone's code. There are management issues, documentation issues, training issues, not to mention "not invented here" issues that can severely limit the actual reuse of code in real life. Ideally, there would be a broad range of third parties that would offer class Frameworks or Foundations for sale that could speed development through common reuse. Although this has happened to some degree such as is the case with the Microsoft Foundation Classes, it has not progressed to the point of having a robust marketplace full of class libraries that address common business problems. The principle reason being that third party class libraries are not exactly "plug-and-play" in that there may be significant changes in the class libraries as new versions are created. The only effective answer to making sure the developers can use them long term is to deliver the actual source code, and with each change in the source code there is a potential management issue that all applications employing that class must be re-compiled and redistributed. From a marketing perspective, that is not a terribly good option, and from an implementer's perspective this carries significant issues in terms of version control, and it is exactly these issues that Microsoft expects to address through the OLE technology.
Although the 'O' in OLE traditionally stood for 'Object', it is important to remember that OLE derives from COM, and it is the 'C', which stands for 'Component', that is the more driving facet of the technology. I have been a software developer for well over 15 years, but I know absolutely nothing, and am in fact VERY dangerous, when it comes to dealing with the computer electronics upon which my software runs. Given that fact, I was still able to personally upgrade my computer memory from 8 MB to 20 MB, upgrade my CPU from an 80486DX 33 MHz to a 66 MHz, and install a 1.4 gigabyte hard drive, all in less than 2 hours. When it comes to how these thing work, I haven't the faintest clue, they just did. All I had to do was follow some simple instructions and plug them in to the appropriate places and the magic occurred. What allowed this to happen was the fact that the plugs, or interfaces, for these electronic items were very well defined as a standard. Therefore, I could unplug my 1 MB SIMMS and plug in my 4 MB SIMMS, and although I don't know how they internally did it, they magically increased the memory on my computer. Wouldn't it be wonderful if software worked the same way. In other words, if I'm not happy with the way my invoicing system processes bills at the end of the month, I can go to my local dealer or catalog and select a new invoice processing module that magically does what I would like it to do, attach it in without having to have my program re-compiled, and it just simply works. And if I'm a programmer, wouldn't it be wonderful to simply select the individual components I need to have for my latest set of requirements from a catalog of components and simply knit them together in the fashion that best addresses the business needs of my customer rather than having to focus a considerable amount of my attention on fundamental technology issues. That, in fact, is the target and the magic of OLE. In short, object technologies are targeted at compile-time reuse, while OLE and component technologies are targeted at run-time reuse, in other words, software "plug and play."
In terms of similarities, both technologies are focused on simplifying the design and development effort. Yes, you heard me correctly, simplifying the process. Obviously, if you examine the reams of information on OLE internals, you would come to the conclusion that the complexity of bringing an OLE component to life is anything but simple. The reality at this point, as you will hopefully see, is that the tools provided with Visual Basic, and to a somewhat lesser extent in Visual C++, allow you to quickly and easily generate the basic components. What is terribly important to understand, particularly to those that may be knowledgeable in procedural languages such as Visual Basic, is that both object and component technologies, if used appropriately, can significantly simplify the design and development process by allowing designers and developers to be able to discuss and validate the design and programming problems in common business vernacular with the user. In object-oriented designs, developers can communicate and program in terms of business objects like invoices, payments, credits, tickets, mailboxes, or any other meaningful business term rather than trying to map the business process to a set of procedures or functions. For traditional Visual Basic programmers this means that designing a public class to operate as a component is not the same thing as developing a set of functions or procedures housed in a module. A class is simply a template which models something from the real world such as an invoice, and each instance of an invoice contains different information and is designed to behave in a predictable "business rule" fashion depending on the information it contains and the other objects it is interacting with. Therefore, a well-designed business component, such as an invoice, would typically be instantiated, or created based on a request from a user service or other business service and would inherently know how to restore and save its persistent information through some interaction with a data service. The consumer of that business component would not necessarily be aware of how, when, or even if it is interacting with the data services, only that it is a usable business component that has meaningful capabilities and instance-specific information that are syntactically meaningful in the context of the business problem.
All of this, although possibly new to Visual Basic programmers, is well-known and documented in numerous object-oriented design texts. What is interesting, however, about OLE centers around the key concept of the binary, run-time compatibility of these objects. In object-oriented languages, in order to make use of the object design, you must be part of the project when it is compiled. In OLE, you can bind to these objects at run time regardless of which language your are using, as long as your language supports OLE component usage. OLE is simply a wrapper that allows anything inside it to be seen and operated upon in an object-oriented fashion. For compiled languages, this thing can either be accessed via low-level, highly efficient interfaces architected to the specifications of COM, or in scripted or interpreted languages it can be accessed via IDispatch or Automation interfaces. The component itself can be physically "wrapped" inside of a DLL or an executable (.EXE), and in the case of Remote Automation, can be physically staged either on the same computer, or a separate server on the network. In all cases, the consumer of the component is completely unaware of this physical packaging and staging. As long as the identification of the component is known, OLE and COM will insure the physical connection and appropriate marshalling of information without any specific dependencies on the part of the consumer of the component. It is this binary runtime compatibility that can provide developers with some interesting design models that can provide some powerful capabilities.
Within OLE, there is the notion of an interface "contract". In order to best understand what is meant by this concept, it is first important to define a contract, which is an agreement between two or more parties regarding the terms and conditions for the execution of business. Take a look at any contract and you will see that there are obligations to be met on both sides of the table. Typically, the contractor agrees to pay the contractee if the contractee delivers to certain specifications. The contractee agrees to deliver to certain specifications if the contractor adheres to his part of the bargain. Depending on how tight the specifications of the contract are, both the contractor and contractee may have considerable latitude in how they accomplish what they have agreed to accomplish. Therefore, the way in which we adhere to the terms and conditions is not really a consideration in most cases, just that we adhere.
In COM and OLE, the term 'Interface' carries with it an explicit definition. In short, an interface relates to a known set of functions and parameters. It is therefore that composite collection of functions and parameters, along with the definition of what they do, and the obligations and sequence of the interactions expected by a user of that interface that constitutes the software "contract" for components. The interface is nothing more than a specification of usage and behavior, and can be implemented in any way that makes sense by the implementer of the component. Therefore, if you were to create an OLE server component in Visual Basic known as "MyProg.MyClass", it is the composite functions and usage patterns of the methods and properties within that class that constitute its contract with the outside world. Because a component's functional entry points are the only things exposed to the rest of the world, it is entirely possible to have completely different implementations of that contract that may, in fact, have different behaviors. Understand that this contract is not documented anywhere in the physical system. What is in the registration database is simply a reference to a given implementation of the interface. There may, in fact, be numerous entries in the registration database that refer to different implementations of the same interface contract.
Another key learning point about OLE is the notion of contract users and contract implementers. Because the interface contract is nothing more than a specification, it can be required of me as a user of a component to implement a given contract specification, in other words, a pre-defined set of methods or properties, so that I can participate in some activity with other components. In this case I become the contract implementer. A great example of this concept is the IShell interface within Windows 95. If I would like to modify how a given object on the desktop behaves when another item is dropped onto it, I can do so as long as I adhere to the pre-defined specifications of IShell interface. In this case, I am the implementer of the contract, and Windows 95 is the user of the contract. Therefore, in designing components, and considering how you would like them to interact with the outside world, some thought should be given to how you expect your consumers to interact with your interface, which in many cases will lead to developing a specification for a contract that they must implement in order to participate and gain access to your services. This can result in a design model that may not be terribly familiar to many developers known as the callback model and is characterized in Figure 1.
Figure 1. OLE callback model
In this case, in order to be a user of the MyProg.MyObj interface, the contract specifies you must implement the Notify interface whose signature consists of a specified set of methods and properties, possibly along with certain expected behaviors. The contract with MyProg.MyObj further specifies that when you use it, you must provide it with a pointer to your implementation of the Notify interface. And when you call certain functions within the MyProg.MyObj interface, they will expect to call your Notify interface in a certain way. Therefore the flow of logic as depicted above is, Component A creates an implementation of the Notify component interface, and passes that object pointer to Component B through the use of one of Component B's defined methods or properties. Then, during the course of interaction between the components, Component A calls Func x in Component B, which in turn calls a pre-defined function within the Notify object of Component A that returns back to Func x in Component B, which subsequently returns back to the calling function in Component A. There are a number of potential uses for this type of design model, the most obvious of which in the Visual Basic environment is to provide a way of refreshing the data in a control on a form when a given function is called in an OLE object. Consider for a moment the situation in which you have an invoice object used by a Visual Basic application. The invoice object has a Reconcile member as one of its methods which takes as its argument a collection of payment objects. Because it is likely that that this method may take a considerable amount of time to complete, it might make sense to expose another method in the Invoice object that allows any user of this component to provide a pointer to a notification object which, if present, will be used during the Reconcile process to send updated progress information. Therefore, a Visual Basic application that uses this object would simply create its own implementation of a notification object and provide it to the Invoice object prior to calling the Reconcile member. Then, within the code of the notification object, it can take the status information sent during the Reconcile process and reflect it to the end user through a progress bar control.
As an example of this technique in both Visual Basic and Visual C++, I developed two very simple projects around the idea of a registration desk at a hotel. The problem being that there may be a number of registration clerks on individual workstations checking in guests. If for some reason a room is not available because of maintenance, the maintenance personnel might call the front desk to advise them, in which case that clerk needs to designate that the specific room is no longer available. That information needs to be reflected to all other registration clerks to ensure that no guests are placed into a room in which maintenance work is being done. In traditional programming, this would typically be accomplished by all workstations intermittently polling a server to determine the availability of rooms, or simply checking the values on a given room when someone tries to check into it. A more elegant model is accommodated through a logical design in which each workstation provides a centralized Room Manager server component with a reference to an object we might refer to as a notification box. The Room Manager can then keep track of the rooms and their individual states and, since it knows the notification boxes of all of the clients, update them individually with any changed information. What is also interesting in this example is that there are two separate projects, one that implement the entire design, both user and business service components, using Visual Basic version 4; the other implementing the user services within Visual Basic 4, and the Business Services in Visual C++ version 2.2 to allow contrasting of physical implementation factors.
First, at a logical design level, it is important to determine the 'signature', or interface contract specification for the Notification Box object. Note that the idea of a notification box would probably not be readily apparent in a usage scenario that might be developed as part of the conceptual design for this application since it does not, in real life, exist. However, a Notification Box-type object might be something that would be useful in the real world were it not a costly and procedurally complicated thing to institute. However, the value in object design is that we can create new functional objects that could be useful such as a Notification Box without the cost or procedural complications that would be incurred in real life, pointing to the fact that a part of good object design starts with creativity in creating new and useful elements that may or may not have any relationship to the way things are done in the physical world. As mentioned before, the logical definition of this interface should be such that it can remain consistent regardless of how different clients in different languages may choose to implement it and could be represented in pseudo-code as:
Interface for Notification Box
variant:result = AlertMessage(string:Message)
Recieves and processes high priority alert messages
Shutdown(variant:seconds)
Shuts down the client application within a specified number of seconds.
variant:result = Refresh(variant:type, variant:values)
Generic message processor for processing a range of messages sent from the Room Manager object. The type value contains a numeric value designating which type of message is being sent. The values variable contains different values depending on the type of message. The currently defined types and values are:
Type Value format
0 String containing error information.
1 One dimensional array containing the four elements of
room ordinal value, room id, status, and guest name
As physically implemented in the Visual Basic Register application, the Notify class adheres to this specification in the following way:
Public Sub AlertMessage(szMessage As String)
'Dont do this!!!
MsgBox szMessage
End Sub
Public Sub ShutDown(nSeconds As Variant)
Register.Timer1.Interval = nSeconds * 1000
Register.Timer1.Enabled = True
End Sub
Public Function Refresh(vTypeUpdate As Variant, vValues As Variant)
Register.lblError = ""
Select Case vTypeUpdate
Case 0 ' error case
gszLastError = vValues
Case 1 ' update room
With Register
.grdRooms.Col = 0
.grdRooms.Row = vValues(0)
.grdRooms.TEXT = vValues(1)
.grdRooms.Col = 1
.grdRooms.TEXT = vValues(2)
.grdRooms.Col = 2
.grdRooms.TEXT = vValues(3)
End With
Refresh = 0
End Select
End Function
The AlertMessage method is simply an example of an inappropriate implementation of this interface in the way that it uses a modal dialog box upon receiving an alert. The problem here being at the physical design level that, since the message box is modal, the OLE server that called back to this method is blocked until the user accepts the message. Not a good idea if multiple clients are concurrently trying to access the server.
The Shutdown method is used by the Room Manager server to tell clients to shutdown in a given number of seconds. This is implemented by using a timer which is initially disabled. Upon receiving this message, the timer is enabled and initialized to an appropriate interval. Within the timer event, the application initiates an appropriate shutdown. This is necessary to allow the callback to return to the server (and subsequently to the calling application) before closing its connection to the server.
The real work in the Notify interface is done by the Refresh method which takes two arguments: a VARIANT type specification for what data it is receiving, and a generic VARIANT containing the data. As shown above, the variant can contain either a single value or an array of values (known in the OLE world as a SAFEARRAY). The type argument is simply used to determine what format is contained in the variant structure and process it accordingly. This points out a very significant performance issue that can arise when dealing with OLE components in that, just because everything can be reflected as a component, doesn't mean that it should be. For example, it would be possible rather than using a generic variant for exchanging information between the client and server, to have the server simply send back an object pointer and have the client retrieve values out of it. The issue here is that each call into that object to retrieve values would incur the calling overhead of Automation, and given the fact that all we are interested in is the data anyway, there is no specific advantage to wrapping that information within a component. Separating whether or not you are simply dealing with data or with an intelligent object that contains both data and processing logic is essential to well-performing component designs.
In order to provide this interface to the Room Manager server, each client must call the Room Manager's Connect method with a pointer to its implementation of the notification box object, as well as call the Room Manager's Disconnect method when they want to terminate their use of the Room Manager's services. Because each client has its own implementation of the Notification component, each with its own unique pointer, this object pointer can be used by the Room Manager server to keep track of all of its active clients. Therefore, whenever any client makes a change to a given room, the Room Manager can simply loop through its collection of client notification box objects to send each client the updated information about the changed room via the object's Refresh method. What is also interesting to note about this particular design is the flexibility of the notification interface. Note that as long as there is a consistent agreement about the types of notification messages that the server will be sending, the clients can either choose to ignore it or process it depending on whether or not the information is useful to them, all without changing the interface or breaking the model, making this notification object highly reusable under a number of circumstances. The emphasizes an important design factor that component reuse is highly dependent upon a flexible and durable logical interface definition.
In examining the server-side of this design, we can see some of the mechanical differences, and potential physical design factors, that may lead to choosing one implementation over the other. What is important to note, however, is that the logical design of this application is consistent, regardless of how it is physically implemented. In fact, if we were to examine the logical object model of this application using conventional Microsoft diagramming techniques, it would look similar to the following, regardless of how we eventually might choose to implement it:
Figure 2. Room Manager Server object model
In this case, the server maintains two different collections of objects, Notify objects and Room objects. The Room collection is the more typical implementation of a set of business objects that are controlled by and instantiated whenever the Server is brought to life. The interesting aspect here is that the Notify collection of objects are controlled by the Server object, but are instantiated externally and given to the Server to manage and use. If a client needs to operate on a Room object, it requests that object via the Rooms() member method of the Server, specifying the ordinal value of the room it is interested in. Once it has been given the Room object it can perform whatever meaningful business operations it needs to on that object such as changing that particular room's status via the Room's ChangeStatus() member function. Aside from the logical model specified above, it is also important to understand, as mentioned previously, the sequence of interactions that are expected in this particular contractual arrangement. It could be specified that it is the obligation of the Room object to ensure all clients are notified whenever a change in status occurs. This would typically be accomplished via an interaction between the Room object and its controlling Server object. However, this may not be the most flexible design in that the Room object would have a direct dependency on the Server object. What if there is a possibility later that that the Room object might be used within other tasks in the business which do not have the need for the Server object? In other words, there may be a billing process that needs to use Room objects in the process, and needs to instantiate them individually without having to go through the Server object. In this case it may be more prudent to specify that consumers of these objects are obliged to tell the Server of any Rooms they have updated so the Server can insure proper notification of all other clients. The Room object would therefore be effectively de-coupled from any dependency on the Server object and potentially be more flexible (that is, reusable).
Now knowing this level of logical interaction detail, along with detail regarding the methods and their arguments, it's possible to explore the different physical characteristics in terms of how the design might be implemented under different technologies such as Visual C++ and Visual Basic. In both cases we know that the Server must be a multiple-use component, meaning that OLE will create only one physical instance of the object, regardless of the number of users. In Visual Basic this is accomplished by specifying the Server Class Instancing property as Creatable Multiuse. In Visual C++ it is not quite so obvious. The easiest way to create a multi-use component in the MFC is to create the project as an MDI application in the AppWizard. The key lines that enable this multi-use aspect are represented in the InitInstance member method of the WinApp-derived application class as:
pDocTemplate = new CMultiDocTemplate(
IDR_HOTELVTYPE,
RUNTIME_CLASS(CHotelvcDoc),
RUNTIME_CLASS(CMDIChildWnd), // standard MDI child frame
RUNTIME_CLASS(CHotelvcView));
AddDocTemplate(pDocTemplate);
// Connect the COleTemplateServer to the document template.
// The COleTemplateServer creates new documents on behalf
// of requesting OLE containers by using information
// specified in the document template.
m_server.ConnectTemplate(clsid, pDocTemplate, FALSE);
In particular, if you examine the CWinApp::ConnectTemplate method description you will see that the third argument, if set to FALSE, will allow a single instance to have multiple instantiations by client controllers (that is, be the equivalent of the Multiuse aspect in Visual Basic).
It is important to realize that in the case of both a Visual Basic multiuse server and a Visual C++ MDI application, that the actual objects themselves are not shared by the clients. What is shared is the packaging, or in this case the executable. In effect, in Visual Basic a new RoomServer class is constructed whenever a client controller does a CreateObject, whereas in Visual C++, a new Cdocument class is constructed for each connection, even though in both cases the executable that houses these classes is created only once by the first controller to create them. From within these classes, information relevant to all of the connections must be stored in some global manner within the executable in order to be accessible by each instance of the class.
In this example the key elements that the server needs to manage across all of the connections are the number of active clients and their dispatch pointers, the number of room objects, and the room object collection itself. In Visual Basic this is done by declaring global values for the application as:
Public gnActiveClients As Integer
Public gCliList(10) As Object
Public gnNumRooms As Integer
Public gRoomList() As Object
within a separate .BAS file. In the C++ MDI application, these values are stored in the CWinApp derived application class:
class CHotelvcApp : public CWinApp
{
public:
CHotelvcApp();
short mNumConnect; // counter for connections
short mNumRooms; // total rooms
BOOL AppendConnect(LPDISPATCH lpd); // add connection
BOOL ReleaseConnect(LPDISPATCH lpd);// delete connection
short FindConnect(LPDISPATCH lpd); // connection lookup
CRoom mRoom[10]; // room object collection
LPDISPATCH mlpdNotify[10]; // collection of connections
The first action a client must take in interacting with the server after creating it is to call its connect method, passing it a pointer to its notification interface. The server must then keep track of this client interface by placing that value into its collection (or array) of dispatch pointers, and then callback the client with updated information on each of the rooms. Examining this startup sequence of events in both languages points out some significant implementation considerations between the two tools.
First, it is important to note that one of the advantages of dealing with OLE, in contrast to other integration technologies such as RPC, is that it inherently supports an automatic activation model. This means that the first client that creates a connection to the server will activate it. Therefore within the server startup code, the Room object collection must be initialized. In Visual Basic this is done within the Form_Load event of the main Room Server form:
Private Sub Form_Load()
Dim nCnt As Integer
Dim vResult As Variant
'Dim and initialize the rooms collection
gnNumRooms = MAX_CLIENTS
ReDim gRoomList(gnNumRooms)
For nCnt = 0 To gnNumRooms - 1
Set gRoomList(nCnt) = New Room
vResult = gRoomList(nCnt).Initialize(nCnt, "", 100 + nCnt, "Avail")
Next nCnt
'Initialize other variables
gnActiveClients = 0
Set gPrimeRule = New NonPrimeRule 'default rule
End Sub
The first thing the server must do is to initialize each Room object with appropriate information. In this case, these values are hard-coded through the Initialize member of the Room object. In a live application, these values might be retrieved from a Data Services interface with the Initialize member simply taking a handle to an ODBC connection, or pointer to a DAO or RDO object as its argument. The last line in the Visual Basic code refers to another global value of gPrimeRule, which is a pointer to a Rule object which we will discuss later.
In the Visual C++ implementation, this same initialization sequence would be placed into the InitInstance method of the CWinApp derived class as:
BOOL CHotelvcApp::InitInstance()
{
// Initialize OLE libraries
if (!AfxOleInit())
{
AfxMessageBox(IDP_OLE_INIT_FAILED);
return FALSE;
}
// initialize room collection
short nCnt;
char szBuf[10];
for(nCnt = 0; nCnt < 10; nCnt++) {
wsprintf(szBuf, "%d", 100 + nCnt);
mRoom[nCnt].Initialize(CVariant(nCnt), CVariant("Vacant"),
CVariant(szBuf), CVariant("Avail"));
}
// initialize connections
mNumConnect = 0;
Of note in the Visual C++ implementation is the use of the CVariant classes for passing the values to the Initialize member of the Room object. The use of this class is an important factor in simplifying the passing of information in mixed-model OLE applications using Visual Basic and Visual C++ and is discussed in-depth later. Certainly, in this case, it would be possible to simply define those values as native C++ types since they are only being called, in this case, from C++ and not from Visual Basic. However, that could potentially raise some conflicts in the future if we want to make Room objects generally creatable outside of this server application. In that case, other uses of the Room object might dictate that they be created and Initialized from Visual Basic. That being the case, in order to maintain a more durable interface that is less likely to change, it is more prudent to pass these values as VARIANTS to increase their flexibility and durability for the future. Remember that the prime directive in OLE is the immutability of the interface. If the interface changes after deployment, we will be forced to implement a new version of the object to insure none of the old applications break. In this case, the use of VARIANTS decreases the likelihood of that happening.
Once the Server object has been instantiated and intialized, the client must call the Connect method of the server, passing a reference to its Notify object. Because this value is simply a long pointer to a Dispatch interface, and each instance will be unique, this value can be used to uniquely identify each client that is connected to the server. The design in this case is that the server simply keeps a list of ten connections and adds each new connection onto the end of that list, keeping track of the number of total connections. Whenever a client calls the Disconnect method, all higher-numbered connections are moved down in the list and the number of connections decremented. This could obviously be simplified in each language through the use of list objects, but for simplicity sake, we will focus in this case on using a simple array to keep track of the connections. Of note, however, is that one of the key aspects of OLE that is most valuable is that we could opt in the future to change this internal design to employ list objects, which would not effect any of the clients since this would not entail any change in the interface. Once a connection is made, the server is obliged to send updated room information to the newly connected client via its Notify object. In Visual Basic, this sequence of events is performed by the Connect method and the RefreshClient method in the RoomServer class:
Public Function Connect(oNotify As Object)
Dim vResult As Variant
'This function collects the client notification
'object into an array, then uses the RefreshClient
'function to send updates on all the room objects
'to the connected client....
If gnActiveClients >= MAX_CLIENTS Then
Connect = False
Exit Function
End If
Set gCliList(gnActiveClients) = oNotify
gnActiveClients = gnActiveClients + 1
RoomSvr.lblActiveClients.Caption = gnActiveClients
vResult = RefreshClient(oNotify)
Connect = True
End Function
Public Function RefreshClient(objClient As Object)
Dim vResult As Variant
Dim nCnt As Integer
For nCnt = 0 To gnNumRooms - 1
vResult = gRoomList(nCnt).RefreshClient(objClient)
Next nCnt
RefreshClient = True
End Function
As shown here, the RefreshClient method simply loops through each of the Room objects in its collection and executes the Room's RefreshClient method, passing it the client's Notify object pointer. Therefore it is each Room object that is actually exploiting the Notify.Refresh method to send Room object-specific information back to the client.
This same sequence of events is handled in a slightly different manner in the Visual C++ implementation. First, because it is the CWinApp derived application class that is maintaining the global connection and room information, it is necessary for the CDocument derived class which is unique to each client connection, to have access to it. In the MFC this is accomplished via the AfxGetApp() function which returns a pointer to the Application object. Rather than do this within each method in the Document class, it is much simpler to get this pointer at document creation time and maintain it as a member variable in the Document class as shown below:
BOOL CHotelvcDoc::OnNewDocument()
{
if (!CDocument::OnNewDocument())
return FALSE;
// TODO: add reinitialization code here
// (SDI documents will reuse this document)
mApp = (CHotelvcApp*)AfxGetApp();
return TRUE;
}
Therefore, within the Connect method of the object or any other method, we simply have to refer to the mApp member to have access to the data or objects we need. Also, since it is the Application that is responsible for maintaining these lists, it makes sense to add several member functions to the Application class for managing this information. This is done through the definition of three new public functions in the application class seen as AppendConnect, for adding a new connection, ReleaseConnect(), for deleting a connection, and FindConnect to search for a specific connection in the array. Therefore, the Connect and Disconnect methods of the Document class are very simple, but do point out one very key aspect of managing external connections:
VARIANT CHotelvcDoc::Connect(LPDISPATCH lpdCallback)
{
short nCnt;
lpd = lpdCallback;
lpd->AddRef();
mApp->AppendConnect(lpd);
for(nCnt = 0; nCnt < 10; nCnt++) {
mApp->mRoom[nCnt].RefreshClient(lpd);
}
return CVariant((BOOL)TRUE);
}
VARIANT CHotelvcDoc::Disconnect(LPDISPATCH lpdCallback)
{
mApp->ReleaseConnect(lpdCallback);
lpd->Release();
return CVariant((BOOL)TRUE);
}
Note that as in the Visual Basic implementation, the connection is either added or deleted from the global collection of dispatch interfaces. However, Visual Basic does a very nice job of managing some of the lower level details such as reference counting for these external objects, that Visual C++ does not. Therefore it is important that we tell COM that there is a new reference to this object so that it does not prematurely release it. This is the reason for the low-level calls to the IUnknown AddRef() and Release() members contained within the object pointed to by the LPDISPATCH. What is deceiving in this case is that without those calls, the RefreshClent method would correctly send the information back to the client. However, subsequent calls to the server that should result in a callback to the Notify object's refresh method would fail with a memory access error. This is because, from COM's perspective, the reference to the Notify object has gone out of scope once the Connect() method is complete, and it therefore dereferences and releases the memory. The LPDISPATCH pointer is still good, but it points to nothing.
After having explored some of the key differences in the implementation of the Server object in Visual C++ and Visual Basic, it is now possible to contrast some the key differences in how they must be used by the client applications, and what is noteworthy here has to do with the way in with automation objects can be bound at runtime. In Visual Basic, all automation components such as the RoomManager Server are constructed so that they can potentially be either early-bound or late-bound. In early binding, the controller application has direct access to the methods of the server using low-level COM interfaces, whereas in the case of late binding, all methods and properties are accessed through a level of indirection in the low-level COM interface of GetIDsOfNames() in the IDispatch interface which determines the function identifier (known as the DispID) of the method and its associated arguments. Once this is done the controller application can call the low-level Invoke() method of the IDispatch to have the function executed. Obviously, if performance is a key design goal of the application, this could be a significant factor in the physical implementation. The trade-off here being that in order to support early binding, the component itself must be engineered specifically with a dual interface. In other words, the component must have a traditional IDispatch COM interface implemented in conjunction with its automation methods. Visual Basic components meet this criteria, whereas Visual C++ components constructed through the normal mechanics in the Class Wizard do not. Therefore, in the Visual Basic implementation of the client Registration application, it can simply include the Server component in its References and instantiate it via the construct of:
Dim RoomMgr As New RoomMgr.Server
within the public declarations for the main form. In the case of the Visual C++ constructed server, it must be instantiated by the Register client application in the more traditional fashion of:
Set RoomMgr = CreateObject("Hotelvc.Server")
within the main Form_Load event. Another consideration with early binding, however, is that because this binding is basically built into the client executable, if there is a problem binding to it at runtime there is no way to intercept that error and gracefully inform the user of the problem. Therefore, if the most important design goal for the application is to be highly robust and supportable, it may make sense to opt for late binding in spite of the performance degradation.
The physical design implications here between implementing the server in Visual Basic or Visual C++ is that, unless there is considerable restructuring done on the Visual C++ application, it may be very possible that the C++ implementation could actually be slower than a Visual Basic implementation. This may not be true, however, if the number and frequency of the calls to the object's member methods were limited, and the processing within those calls were relatively complex. In that case, the performance degradation incurred through late binding might be offset by the fact that the C++ implementation could perform the complex operations in a much more efficient manner than Visual Basic.
All is not lost, however, in terms of binding and performance in the MFC implementation of the server. It is important to note that the Server component in this design must be both an automation component and an automation controller in that it must employ the notification box interface provided to it by its client applications. This points out yet another way of binding to Automation components which is implemented in the Class Wizard and MFC classes referred to in the COM specification as "ID Binding."
In ID Binding, rather than depending on the GetIDsOfNames function of the IDispatch interface to get calling information, the DispID is determined ahead of time and hard-coded into the source code. In Visual C++ this happens when you use the "Read Type Library" button on the Automation tab of the Class Wizard dialog. Normally, this would be used to access a compiled or source code version (.TLB or .OLB) of the type library which specifies the methods and properties defined within an OLE component. Visual Basic, however, incorporates this type library information directly into its executable. Therefore, in order for a Visual C++ Server component to exploit the Notify component of the Visual Basic Registration client app, you would simply specify the Visual Basic executable (.EXE) file from the "Read Type Library" button and select the Notify interface as the one you want to be able to use. The class wizard would then create a COleDispatchDriver derived class for you containing the following member methods.
class CNotifyVC : public COleDispatchDriver
{
// Attributes
public:
// Operations
public:
// method 'QueryInterface' not emitted because of invalid return type
// method 'AddRef' not emitted because of invalid return type
// method 'Release' not emitted because of invalid return type
// method 'GetTypeInfoCount' not emitted because of invalid return type
// method 'GetTypeInfo' not emitted because of invalid return type
// method 'GetIDsOfNames' not emitted because of invalid return type
// method 'Invoke' not emitted because of invalid return type
void AlertMessage(BSTR* szMessage);
void ShutDown(VARIANT* nSeconds);
VARIANT Refresh(VARIANT* vTypeUpdate, VARIANT* vValues);
};
Notice that there are a number of low-level COM interfaces contained within that executable for supporting early binding that the Class Wizard does not recognize because it is simply looking for valid automation functions. The other functions such as QueryInterface or GetIDsOfNames return HRESULT values, which the Class Wizard perceives to be non-viable automation return values. What is noteworthy for this discussion, however, is that the implementation of the Refresh automation method as constructed by the Class Wizard is:
VARIANT CNotifyVC::Refresh(VARIANT* vTypeUpdate, VARIANT* vValues)
{
VARIANT result;
static BYTE BASED_CODE parms[] =
VTS_PVARIANT VTS_PVARIANT;
InvokeHelper(0x60030024, DISPATCH_METHOD, VT_VARIANT, (void*)&result, parms,
vTypeUpdate, vValues);
return result;
}
Note that the first value in the InvokeHelper member is actually the DispID, which is compiled into the code. This means that a call to the Refresh member of the Visual Basic Notify component will not have to first call the GetIDsOfNames function, but instead will directly call the IDispatch Invoke method, reducing by half the amount of overhead in the call.
As mentioned previously, there is a considerable assumption being made about the flexibility of using VARIANT values in this particular design. In fact, the use of VARIANTS is pretty well inherent to the nature of automation as implemented in VBA. The MFC in its current state does not provide any additional value around the use of VARIANTS beyond the basic functions provided by OLE. As such, dealing with VARIANT variables in C++ is considerably more complex than in Visual Basic where VARIANTS are the default and there is considerable support for them. This limitation within the MFC can be overcome however through exploiting the CVARIANT class that was defined by David Kruglinski in his Inside Visual C++ book. In the book he provides a CVariant class which can be very useful when dealing with VARIANTS, but still has some limitations when dealing with arrays constructed out of VARIANTS or what is known in the OLE world as SafeArrays. An additional issue is that variant array string values passed by Visual Basic are actually constructed as wide characters or UNICODE. Therefore, I extended the CVariant class as defined by Kruglinski to better accommodate and simplify these variable passing issues between Visual Basic and Visual C++.
To account for the UNICODE issue, I added an additional method to the CVariant class called GetWideChar. This takes as its argument a pointer to a UNICODE wide character and initializes the CVariant as a normal zero-terminated C-type string. More important, however, is the addition of the CSafeArray class which can be used to significantly simplify dealing with SafeArray manipulations in MFC applications. In particular, the Retrieve member of the CSafeArray class is used to extract values out of a specific element in a SafeArray (potentially sent by a Visual Basic application) into a CVariant. If you look at the Retrieve code you will see that it automatically tries to detect whether or not the value is a double byte character set string, or a normal string. If it is UNICODE, it exploits the GetWideChar member to reduce it to a normal BString value usable in C++. This is valuable in allowing a business component to remain unaware of whether or not its services are being used by Visual Basic (which sends UNICODE), or another business object (which probably uses normal BString values).
To examine how all of these elements come together in a physical implementation of the Server object, it is probably easiest to examine the RefreshRoom member function of the component constructed in the Visual C++ rendition of the Room Manager Server.
VARIANT CHotelvcDoc::RefreshRoom(const VARIANT FAR& RoomNum)
{
CSafeArray sa(4);
short nCnt;
CNotifyVC Notify;
sa.Assign(0, &CVariant(mApp->mRoom[RoomNum.iVal].mOrder));
sa.Assign(1, &CVariant(mApp->mRoom[RoomNum.iVal].mRoom));
sa.Assign(2, &CVariant(mApp->mRoom[RoomNum.iVal].mStatus));
sa.Assign(3, &CVariant(mApp->mRoom[RoomNum.iVal].mGuest));
for(nCnt = 0; nCnt < mApp->mNumConnect; nCnt++) {
Notify.AttachDispatch(mApp->mlpdNotify[nCnt], FALSE);
Notify.Refresh(&CVariant((short)1), &sa);
}
return CVariant((BOOL)TRUE);
}
As shown above, the RefreshRoom member of the normal Document class of the application is constructed as an automation method in the Class Wizard which takes a VARIANT argument and returns a VARIANT value. This particular method is called by the Register client after it has updated information on a room. It is responsible for looping through its collection of Notify objects (maintained as a list of LPDISPATCH pointers in the CWinApp derived class) and sending updated information to them through the Refresh method of their Notify objects. The format of this information as discussed earlier is that the first argument will contain the type of message being sent (which in this case is format #1), and the second argument will contain an appropriate value based on the type of format, which in this case is an array of values containing the room order, room number, status, and guest name. The constructor for the CSafeArray takes either one or two arguments depending on how many dimensions will be contained in the array. Therefore the sa value in the code above initializes a one dimensional SafeArray containing four elements and is equivalent to the traditional C++ array definition of sa[4]. Notice also the use of the CNotifyVC class used which is the class imported via the Import Type Library function of the Class Wizard. In this case, the Server does not actually maintain a collection of CNotifyVC objects, but rather simply maintains a list of dispatch pointers as a normal array of LPDISPATCH values in the CWinApp derived application class. This is done for simplicity since the only thing of value in the interaction with the client is the dispatch pointer, not the C++ class which can be instantiated on the fly to accommodate the callback. You can see that this is done in the for loop which cycles through the array of LPDISPATCH pointers (each representing different clients), and attaching that dispatch pointer to the Notify object, then making the ID-bound Refresh call to its member function sending the appropriate VARIANT information. The four lines above this for-loop are used to populate the SafeArray with Visual Basic compatible information from the Room collection of objects maintained as another array in the CWinApp derived application class.
Although the Room Server does not have any member functions that are engineered to receive SafeArrays within their arguments, the following code exemplifies how you would construct a C++ automation method that does:
VARIANT CBTestDoc::SendCSafeArray(const VARIANT FAR& Array)
{
// receives a single dimension SafeArray and extracts items.
CSafeArray sa;
CVariant cvt;
sa = Array;
sa.Retrieve(1, &cvt); //possible wide char
sa.Retrieve(0, &cvt);
return CVariant((BOOL)TRUE);
}
VARIANT CBTestDoc::SendCSafeArray2d(const VARIANT FAR& Array2d)
{
// receives a two dimensional SafeArray
CSafeArray sa;
CVariant cvt;
sa = Array2d;
sa.Retrieve(0, 0, &cvt); // possible wide char
sa.Retrieve(0, 1, &cvt);
sa.Retrieve(1, 0, &cvt); // possible wide char
sa.Retrieve(1,1, &cvt);
return CVariant((BOOL)TRUE);
}
As you can see, the above code is oblivious to the type of variable contained in the SafeArray. In order to use these values you would simple examine the cvt.vt value for the type of value, and based on that use the appropriate member of the union for the value itself (that is, cvt.bstrVal, cvt.iVal, and so on).
Another very interesting design model enabled by the run-time reusability aspects of OLE components is what is referred to the COM Specification as Interface Polymorphism. This is one of the more powerful capabilities of the technology, and carries with it some rather far-reaching implications in terms of the way software components may be engineered in the future. This model is reflected in Figure 2.
Figure 3. OLE run-time binding model
In this situation Component 1 expects to use a given set of methods within a pre-defined component interface specification for Component 2. The specification may be implemented in several different components, each of which applying their own, different processing logic. At runtime a decision can be made as to which is the most appropriate Component 2 (a or b), and that instance of the component is instantiated. What this provides is a way for Component 1 to not have to be overly concerned about the actual business logic associated with a given transaction, it is only concerned with determining what are the key variables and processing entry points necessary to carry out the business logic and define that as the interface specification. This allows other programmers to develop implementations that address their specific needs simply by developing components that adhere to the specification and configuring the master component at run time to know which implementation to use. The implementation possibilities of this are endless in that the vendor of an application can simply pre-determine the interface and pre-publish it even before the product is launched. The vendor might provide a default implementation which does some known processing and, through the magic of component containment, allow the developers of the externally-developed components to either implement their logic in addition to the default logic, or simply replace it in total. There may even be a day in which many verticalized business applications simply implement a general framework and provide buyers of the application with the interface specifications to completely customize their implementation.
As an example of this type of design model, the object model for the Room Manager Server component was extended in the Visual Basic version of the implementation in the following manner:
Figure 4. Extended Room Manager Server object model
In this case a Rule component was defined that contains the validation logic that should be applied whenever a Room component is updated. The interface specification for this component consists of a single member function called ExecRule() which takes as its parameters the object pointer to the client notification box, and the value that is to be applied to the status member of the Room object. It is functionally required to either allow or disallow this change by returning a Boolean value. It is also obligated to inform the client via its notification box as to the reason for disallowing the entry.
From a logical design perspective, this allows the administrator of the server component to arbitrarily decide during runtime whether the prime hours rule is in effect (which does not allow any room to be placed into maintenance status), or the non-prime hours rule should be applied which allows all status changes. Naturally, given this design it would be possible in the future to determine other conditions which require different types of validation logic to be applied. When this happens, the new logic can simply be built as a new implementation of the rule interface, and the Server application extended to allow the administrator to apply that new rule. In terms of the consumers of this business component, they would not necessarily need to be aware of that change unless it were to result in a new message format being sent via their notification box interfaces.
At the physical design level this is implemented in the Visual Basic version of the Server by adding a new global object variable known as gPrimeRule. This value is initialized at Form_Load time to point to the non-prime rule implementation as:
Set gPrimeRule = New NonPrimeRule 'default rule
The user interface for the Server provides a check box which allows the administrator to specify whether or not prime hour rules or non-prime hour rules are in effect. If you examine the Click event for that button, you will see the following code:
Private Sub ckPrimeHours_Click()
Set gPrimeRule = Nothing
If ckPrimeHours.Value = 1 Then
Set gPrimeRule = New PrimeRule
Else
Set gPrimeRule = New NonPrimeRule
End If
End Sub
Obviously, the above code disengages the current rule pointed to by the global gPrimeRule variable and attaches the appropriate rule object based on the value of the check box. Within the ChangeStatus member of the Room class you can see how the currently attached rule is applied whenever the client makes a call to this member function.
Public Function ChangeStatus(oClient As Object, szStat As String)
Dim vResult As Variant
vResult = gPrimeRule.ExecRule(oClient, szStat)
If vResult = 0 Then
szStatus = szStat
End If
ChangeStatus = vResult
End Function
As you can see from the above code, the ExecRule member is always called. The particular behavior of the ExecRule is determined by whatever object is currently attached to the gPrimeRule object variable. The two implementations of the Rule objects currently supported by the server are defined as the PrimeRule class and the NonPrimeRule class and are shown below:
(PrimeRule.CLS)
Public Function ExecRule(oClient As Object, szStat As String)
Dim vResult As Variant
If szStat = "Mnt" Then
vResult = oClient.Refresh(0, "Maintenance cannot be applied during prime hours")
ExecRule = 1
Else
ExecRule = 0
End If
End Function
(NonPrimeRule.CLS)
Public Function ExecRule(oClient As Object, szStat As String)
ExecRule = 0
End Function
Clearly, both classes must adhere to the definition and general behavioral characteristics defined by the interface specification, although each of them implement specific behaviors that relate to their usage. In the case of the NonPrimeRule, it simply returns success which allows the Room object to continue with its update. In the case of the PrimeRule, if the status is being changed to "Maint", it notifies the client via the Refresh() member of the notification box object and returns a non-success value which signals the calling Room object that the update was disallowed.
The most prevalent potential applications for this type of design model within the business world would relate to those types of problems in which there are a wide range of business situations which could flavor how a given business process or rule is to be applied, in other words, exceptions. Most typically these are situations in which a business object such as an payment might be processed in one of many ways depending upon the current state of the customer, the time of year, or any number of other variables. The business need is to have a system in which they can flexibly and quickly add new payment programs for their customers without having to significantly affect their code base or redeploy their entire application. This presents a fairly dicey problem in traditional technologies, but can be handled rather elegantly using components by simply defining the key methods and properties of a payment program object and simply configuring into some data source which component should be used under a given circumstance. In this way, the deployment of a new payment program would simply entail the development of the payment program component and an appropriate entry into the data source regarding the conditions for its use.
So, given the previous examination of the Room Manager component model, are there any lessons that can be derived regarding how to best implement it? Clearly there are some performance issues that need to be dealt with in terms of the current state of the tools. Other considerations in this case might deal with concurrency issues in terms of the number of consumers of the Room Manager components, the frequency of access, and the amount of information to be exchanged. One of the key limitations within Visual Basic for dealing with high-concurrency servers is the fact that there is no support for threading. Given a high number of potential users, it might be more prudent to sacrifice the performance gained through early binding in order to be able to develop a multi-threaded server within Visual C++. Alternatively, it may be worth the additional effort to engineer the C++ server component so that it can, in fact, be early bound. Most of these types of issues can only be resolved explicitly by returning to the core design and business goals for the project. Although there may be great technical reasons for implementing things a certain way, they may not make sense in terms of the business issues surrounding the cost of development, maintenance, deployment as well as meeting the business window of opportunity. This is where a good grasp of the principles of versioned releases may help to negotiate the design to an appropriate win-win decision in which the immediate business target can be met, with a long-term focus on performance enhancements.
Clearly, if there is one key lesson to be extracted from this exercise it is the fact that, in the OLE world, everything centers around the interface contract. By divorcing yourself from the physical implementation issues during the logical design of your application you stand a much better chance of developing a robust and durable interface specification that is more likely to stand the test of time. What follows is a short checklist of items that might be useful in evaluating your interface, along with my answers for how I think I did during this iteration through the Room Manager design process:
Do all of the components, methods and properties have business-meaningful names? |
Generally speaking I would say this design rates about a 50%. Were I to iterate once more through this design I would probably opt to make the members of the notification box object more meaningful. For example, rather than a Refresh() method, I might prefer something like a simple Send() which would really be a better indicator of what it is doing. |
Are there any assumptions being made in terms of the interface specific to the type of tool that may be used to build the consumer of this component's services? |
I think overall that the design of the Room Manager shows that it is fairly resilient and free of assumptions in this regard. Clearly the decision to adhere to the usage of Variants throughout the design helps. On the next iteration I would ensure that Variant usage is consistent throughout all of the interface specifications. |
Are there any arguments that are being reflected as objects that should really be data, or vice versa? |
This relates to whether or not what I am passing really should be an object pointer or a complex data type. Certainly, if I am doing nothing more interesting than extracting or placing values into the object, its value as an object should be suspect. In terms of this design, I think it is overall pretty solid in this regard. Given the fact that there is little done with the Room object other than pushing a value into it via its ChangeStatus() member, an argument could be made as to whether or not it is even viable to have the consumer deal with the room object rather than simply sending the status update directly to the Room Manager component. |
Are there any assumptions in this design that would force it to be packaged or staged in a certain way? |
Although there is an underlying assumption in this case that the Room Manager will be servicing multiple, simultaneous consumers; If the Room Manager were to be staged and packaged as a single-use component, it would work the same. |
Are there any other objects that might be derived that, although they do not exist in the real world, might be useful within the object design? |
I think the notification box is an example of how this design exploits the capability to create a useful object that really has no correlation to the physical world, but adds value to simplifying the way in which the components need to interact in order to fulfill the business requirement. |
Are there any methods within the components that might be more useful and durable to be defined as a transaction object? |
Unfortunately, this design is not complex enough to demonstrate this type of issue, but there are certainly cases in the real world in which I might have a business object such as an Invoice that has a MakePayment() method which could potentially be more meaningful and durable if it were to be defined as an InvoicePayment object with a Process() member function. This would be particularly true if an invoice payment potentially needs to be initialized with a number of other business objects so that it can process and manage the entire transaction as a stand-alone entity. |
These represent just a few key considerations that might be applied to a logical design to determine if it is really stable enough to withstand the test of time. Certainly, there are going to be changes to the business that may force a certain amount of remodeling of the object model, but taking the extra time to validate the logical design can make a significant difference in how soon that may have to be done.
As you can see from this discussion, the concepts of OLE are not particularly complex. Often the underlying complexity of the mechanics of the technology get in the way of our effectively understanding and using them. The addition of OLE-oriented development tools like Visual Basic 4 and Visual C++ and the MFC to the arsenal of component developers will undoubtedly provide some key abilities for empowering more robust designs and applications than have been possible under other, more traditional technologies. The problem with powerful tools, however, is that given an inadequate understanding of the key underlying concepts they represent, they will often appear to be great tools that either go unused or underutilized. Developing an understanding and appreciation for what COM and OLE have to offer in terms of design benefits, and re-focusing the design and development efforts to attend to the more critical issues surrounding the immutability of the interface is crucial for all developers, regardless of their language of choice, to allow them to design and build to the potential of this technology.