DBMS

Object-Oriented Analysis and Design Part 2

By Michael Gora
DBMS, September 1996

Guidelines for applying object modeling and object orientation to your applications.


Client/server (and Internet/Intranet) applications require methodologies that deal well with user interfaces and database transactions. Most articles about object-oriented analysis and design (OOAD), however, focus on methodology as it applies to generic complete applications, and they frequently ignore the user interface entirely.

In this follow-up article to my previous article on OOAD (DBMS, June 1996, page 61), I focus on object orientation and object modeling as they apply to interactions with a relational database and applications whose functionality is determined heavily by the user interface. In my experience, the vast majority of client/server applications involve a fairly direct transformation of data from a user interface to a database.

Yes, there are always business rules that go beyond the standard problems of New, Save, Delete, and Validate functionality. But these tend to make up a far smaller part of the typical application than most pundits believe.

I always tell my programmers and analysts that the first three commandments of object-oriented (OO) development are to generalize, generalize, and generalize some more. I also tell them to be certain that all objects of a given class, such as {window}, must have a shared common ancestor in the application so that all windows may share at least some behavior if required. And I tell them that if they do not provide these common ancestors, they will almost certainly regret it.

One of the biggest issues in software development is the definition of objectives for programmers and analysts. I recall a study about 20 years ago in which five programmers were given the same program to write, but each person was given slightly different objectives, such as maximizing code readability, optimizing memory usage, and optimizing CPU time usage. Not surprisingly, each programmer met his or her objectives. But what should a programmer's objectives be? I see a significant number of programmers working on incorrect objectives all the time. What should the objectives be for OOAD

Most of the OO and GUI languages available today are fairly new. Many developers are learning a language as they work on an application. After doing some heavy OO development from 1987 to 1989, I started my first Windows development project in 1989, using SQLWindows as the tool. Because it was a client/server database application, much of my time was spent trying to figure out how to write database code in each window. I spent a lot of time cutting and pasting code from one window to another and then modifying it slightly, because I didn't have the experience with the tool to generalize my problems correctly and SQLWindows didn't have any OO capabilities at that time.

With class libraries supporting OO features in pre-1994 SQLWindows, PowerBuilder, and a few other tools, I was fortunate if generalized code supported 30 percent of our New/Save/ Delete and other transaction requirements. Currently I teach boot camps on OO client/server development using class libraries. My students, who have been using an OO tool for a while without much understanding of object orientation, regularly say to me, "Gee, in all the work we have done so far, we wrote 10 times more code then we needed to." Generalizing the problem and the solution can save a large amount of work if done correctly.

Today, I continue to make it my goal to automate 100 percent of the transaction processing in a window or other object, but of course I never quite meet this goal. The class libraries are getting better and better, but there are always some requirements that they don't seem to meet and that therefore require much code to be written. First, let me describe a few rules of thumb for object modeling in client/server applications. Generally, OOAD focuses on describing objects that fall into one of three partitions:

UIP objects describe the user interface, SMP objects describe technical areas of the system such as error handling and message handling, and PDP objects describe the business rules. In addition to the objects that must be built in programs (in PowerBuilder, Delphi, SQLWindows, Java, and so on), I describe some objects that may need to be built inside a relational database.

Following are some guidelines and options for object modeling. Guideline 1 applies to UIP objects, Guideline 2 to SMP objects, and Guideline 3 to PDP objects. Most OOAD methodologies recommend building the PDP object model first. Most of the OO client/server applications that I have seen, however, have started with the UIP and the data model. I believe that it is often more natural to build the data model before the object model, and that a good object model can be mapped to any data model - so I have no problem building the UIP and data model first. Although I have developed systems in which the PDP object model came first, this approach requires more experience than any other if you want to avoid the dangers of having an object model that may not map well to a data model, programming language, or user interface. For the majority of client/server applications, a prudent risk management approach requires that the UIP and data model come first, so I shall discuss UIP object models first.

Guideline 1

Define a clean window hierarchy and avoid too many window classes.
The simple (partial) application that I discuss in this article is depicted in Figure 1. It consists of three windows: a New/Open modal dialog for finding and identifying a client, a tab folder sheet window for viewing and updating client data, and an MDI frame window that contains the other two windows. The object model for just these three windows has a surprising number of possible class hierarchies, including three major different forms that I will discuss later. I invite you to rate these three object models and send me an email ranking them into first, second, and third place. You may also nominate object models not included in this article. I shall try to publish the results.

To build the application, I need three windows that will appear to the user. I need a frame window, w_frame. I need a find dialog, w_find_client. This dialog has typical New and Open options for finding existing clients or creating new ones. And I need a tab folder window, w_client, for displaying client data or adding data for new clients.

The simplest useful object model for these three windows (see Figure 2) contains a total of six ancestor windows. Note that all windows have a common ancestor. This object model is not supported in all languages because not all languages support a common ancestor for different window types. For example, in SQLWindows, sheets and response windows cannot have common ancestors, but in PowerBuilder they can. In SQLWindows, therefore, the common ancestor w_ancestor would not exist. In my experience, it is always useful to provide a common ancestor for objects of a given type. I have never regretted doing so, and I have written lots of code in which I would have regretted not having such an ancestor.

Because not all sheets will have tab folders, a sheet ancestor for sheets with tab folders w_ancestor_sheets_tabs is inherited from the generic sheet ancestor w_ancestor_sheet. Similarly, because not all response windows will be New/Open, the (find) window w_ancestor_find is inherited from w_ancestor_response.

The most complex object model for these three windows, and one that is becoming increasingly popular, is depicted in Figure 3. The assumption with this model is that for every abstract and concrete window class in the class libraries, another window with the suffix _delta will be added. (Abstract classes have curly brackets surrounding their names and are never instantiated - they are used only as ancestors. Concrete classes are instantiated at runtime.) Each additional delta window provides a placeholder in which the developer can make changes to class library behavior without modifying any object in the class libraries. Note that the effect of this is that for three real windows, you need 12 ancestors and up to six levels of inheritance!

What do you think the result of this architecture will be on performance, memory usage, and program size? Because the memory I require in my laptops has doubled every year for the past four years, more and more developers must be using such architectures. With good memory managers that keep classes persistent in memory so that every class must be built only once, the performance impact is minimal, but memory and program size are still affected heavily. Perhaps we are currently in a phase of overusing objects. Do our applications really require the 2MB and 4MB executables that are becoming commonplace?

If the application is very small, or if the development tool has a significant performance penalty associated with inheritance, Figure 3 represents overkill. But if the development tool has little or no penalty for inheritance and the application is likely to grow, this structure will maximize reuse and minimize maintenance.

Let's consider improving Figure 2, in which there are a total of nine classes (six ancestor window classes and three concrete window classes). If I consider the functional area of processing client data (which requires two windows, the find window and the client window) to be one of five such functional areas in the application (see Figure 4), then I need eight more windows for the missing four areas, for a total of 17 windows to support five functional areas. Using a smart object manager that minimizes the performance hit of inheritance, you use a great deal of memory. Is there another solution?

In Figure 5, the 17 windows have been replaced with five by juggling the concepts of classes and instances. In Figures 2 and 3, the window w_find_client is a class that had only a single instance. This is extremely inefficient (but easy to do) and is much of the reason for the high memory requirements of many applications. An efficient class should always contain a set (that is, more than one instance), and the instances of the class should be defined in the database. The properties unique to each instance are stored in a database and applied at runtime when the class is instantiated. Instead of creating an inefficient class w_find_client (and four similar classes) with a single instance each, you can eliminate by defining it as an instance of the class w_ancestor_find, which is no longer an abstract class but a concrete class. The attributes that distinguish the different instances of w_ancestor_find are stored in a table in the database.

A major conceptual leap has now been made, because something that was originally designed as a class is now an object instance, which is stored in the database. Furthermore, the code is much smaller and faster. But what has been lost? Because today's client/server development tools only let you design classes (visually at design time) and not instances, the ability to view the find windows at design time has been lost. This may disturb the inexperienced designer, but the architects among us should be able to design class library behavior that always makes it easy to view all instances of a class generically at runtime. I have built several tools to facilitate creating such instances.

Which version of the object model did I implement? The original prototype was developed with a fairly inexperienced team under tight time constraints. It was easiest for me to lead the team using the model in Figure 2, because building the ancestors for the model in Figure 5 would have led to idle time and delays for several of the developers. I have recently started another project with a very similar user-interface approach in PowerBuilder, and for this I am using the model in Figure 5. Many of the users of the new application will have low-end machines, and the model in Figure 5 will provide better performance and require less coding than any of the others.

Are there any reasons other than performance for not using the delta classes as drawn in Figure 3? First of all, the application uses a set of custom class libraries that were designed just for this project. There is no external vendor who will be publishing a new version. In addition, if a need for the delta classes did arise, they could easily be added after the fact. There is simply no reason to have them in the first version of the system.

In my experience, class library architectures change so much from one year to the next that even if you expected to be able to plug in a new set of base classes in a year, they would very likely not work without significant code adjustments. And I find it inefficient and silly to have a lot of empty classes just because I might need them, when I know I can always add them later. Putting them into the original object model without attributes or methods of their own seems to be a very awkward design. A good rule of thumb is simply not to implement classes that have no attributes or methods of their own, and to remove them from purchased class libraries if they are not needed immediately.

Guideline 2

Generalize and encapsulate the database services and other system services.
At the simplest level, generalizing the database services means providing class library support so that most windows do not need any code to provide New/Save/Delete functionality. For windows that show data from just a single table, such support is easy to provide in Delphi, PowerBuilder, SQLWindows, and other OO languages. For windows that show data from multiple tables, generalizing the database services is somewhat more difficult but can be done in such a way that the most common cases, such as master/detail windows and most tab folder windows, are handled generically.

The services are usually provided by methods (events or functions) in the ancestor window. If the language is PowerBuilder, for instance, and the code is implemented in functions, then the functions would typically be named wf_new, wf_save, and wf_delete (wf stands for window function). In SQLWindows or Delphi, the naming conventions would be slightly different, but the approach would be the same. In any OO language, the methods could be implemented in events or functions. These functions are usually called by menu items and/or buttons on the window.

Whether a method is implemented in an event or a function depends on your answers to the following questions:

Most of the time, the code for a menu item performs one of just four actions: open a window, close a window, call a single function (such as PrintSetup), or invoke a method in a window. Typically, therefore, most menu items will invoke a method in the window, and when you have a good set of class libraries, many of these methods will have no code except in an ancestor window. If a window w_client for displaying client information is inherited from a window w_ancestor_sheet that has code in the methods I just named, then window w_client should not need to contain any code to provide New/Save/Delete functionality. This would be implemented quite differently in PowerBuilder, Delphi, and SQLWindows, but the same concept applies to all tools.

To be more realistic, let's add four more methods. A window might require "VCR" buttons to provide scrolling among records. If the menu and toolbar have four such buttons, then the ancestor window probably has four methods named wf_scrollfirst, wf_scrollprevious, wf_scrollnext, and wf_scrolllast. A real window w_ancestor would have all four of the previously mentioned methods and possibly a few more. Code in any method can be extended or overridden in a descendant class. Extending a method means that when the method is invoked in the descendant, the code in the method in the ancestor is run before the code in the descendant. Overriding the ancestor means that the code in the ancestor method will not be executed automatically when the same method in the descendant is invoked. If the code in the descendant must be run before the code in the ancestor, you can accomplish this by overriding the ancestor method and instead providing an explicit call to it at the appropriate point in the script.

The goal is to create code in the ancestor that does not need to be overridden. Furthermore, if a particular window does not support updating or scrolling, it may not be necessary to override the ancestor code because there may be no menu items or buttons that ever trigger the code.

Taken to a very useful extreme, the concept of generalizing the database services means encapsulating all database services such as connect, disconnect, retrieve, update, commit, and rollback. I can create a nonvisual database services object n_services_database containing functions nf_connect, nf_disconnect, nf_retrieve, nf_update, nf_commit, and nf_rollback (nf stands for nonvisual function). The window functions would then call the services in the nonvisual object. If there is any reason to believe that you may have to distribute some database operations onto a server, such encapsulation is extremely important. Once you start working with distributed objects, you may wish to move database operations onto a server. If these operations are all encapsulated, it is fairly easy to do. If they are not encapsulated, it is extremely difficult.

Keep in mind that if you distribute some database operations onto other processors and keep some local, you not only need twice as many database connections but you must also deal with distributed transaction concepts such as the two-phased commit protocol. So it may be easier to move all of the database operations to a server, rather than just a few.

A database service object is an important feature of a client/server object model. It is not implemented well by any major development tool or by most current class libraries but is very easy to add to a class library.

Error-handling services, message services, object-management services, platform services, and other services should also all be encapsulated into service objects. Each of these services may require an object hierarchy of its own. But all nonvisual service objects should have a common base ancestor when possible. For example, platform services may include different objects to handle Windows 3.1, Windows NT, and Macintosh. Checking available resources is one operation that is different on different platforms, and platform-specific service objects are an increasingly important requirement.

Depending on the language, it may or may not be important for all nonvisual service objects to have a common ancestor. For example, if the language doesn't provide an automatic mechanism for destroying nonvisual objects at application shutdown time in order to avoid potential memory leaks, then rather than leave this to the responsibility of the application programmer, it should be handled automatically by the class libraries. If every nonvisual object has a common base ancestor that registers it with a central object manager when it is created, then at shutdown time the object manager can automatically destroy all objects that the programmer forgot to destroy, thereby avoiding memory leaks. An object management service object as I just described is very important in PowerBuilder and Delphi, but it is not needed in SQLWindows and Java because memory is cleaned up automatically at shutdown time.

Guideline 3

Encapsulate business rules.
When implemented correctly, business rules will be encapsulated into nonvisual objects. For instance, rules for dealing with a customer would be encapsulated into a class n_customer. The extent to which this is done depends on the experience of the developers, on the language, and on the complexity of the application. The more complex the application, the more important it is to isolate and encapsulate business rules to make them readable and maintainable.

In a simple application, on the other hand, a developer may know that data for a database column will never be entered or updated from more than one window. In this case, if you encapsulated business rules somewhere other than in that window, it would be overkill.

Languages such as PowerBuilder and Delphi support nonvisual database access objects, which PowerBuilder calls datastores. SQLWindows and Java do not support such objects. Business rules could be stored in "primitive" nonvisual objects that have no smart data access and buffering capabilities or in datastores. Placing business rules into nonvisual objects provides a "purer" architecture than placing them into datastores, but it complicates things significantly by requiring windows to be linked to datastores as well as nonvisual objects. Generally, I prefer to think of a datastore object as a nonvisual object that has an optional data access and data buffering component. Instead of placing my business rules for customers in a nonvisual object n_customer, I prefer to place them in a datastore ds_customer. If I use both a datastore and another nonvisual object, coding becomes much more complex, and readability, maintainability, and performance suffer because the number of object interactions increases. I always prefer architectures that minimize implementation and maintenance risk, and I therefore try to avoid overuse of objects.

Despite the importance of encapsulating business rules, it is difficult to write generically about how to do it - the object models required are very different in the four languages I have mentioned. A good abstract object model of a problem domain class simply cannot be implemented easily in any Windows 4GL because of the fairly tight coupling in each of these languages (except Java) between the database and the user interface. Removing that coupling reduces performance and adds complexity, so I typically want to create an object model that takes advantage of the features of the 4GL.

The State of OOAD

In closing, I would like to reiterate the indirect conclusion of this article: There is no single best way to design an OO application. Over the past year, I have seen several class library architectures go from being insufficiently OO to being overly OO. I have also seen a few OO class libraries that are complex, large, and useful. And I have seen simple, small, and useful class libraries.

One significant change this year in support for OOAD for client/server applications is the emergence of new OO CASE tools for client/server development. LBMS System Engineer and Popkin System Architect have been improved significantly, Rational Rose is focusing more on client/server development with 4GLs instead of just C++, and Riverton Software announced a major new CASE tool that takes advantage of the best features in several OOAD methodologies and combines them to support client/server applications development.

I am delighted that the tools and methodologies finally seem to be converging to make OOAD for client/server development well-supported. As the development tools have become more OO, the OO CASE tool vendors have become more interested. Over the next year, I expect the rate of convergence to increase and I expect the OOAD CASE tool market for client/server to leave its embryonic phase and finally enter a high growth phase.


Michael Gora is the president of OSoft Development Corp. in Atlanta. He is a certified instructor for Centura Software Corp. and Powersoft Corp., and he has written several articles on object orientation and client/server application architecture. Gora is a frequent speaker at conferences and has recently designed a course on Java. You can email him at 72302.1506@compuserve.com.


* Borland International Inc., 100 Borland Way, Scotts Valley, CA 95066; 800-245-7367, 408-431-1000, or fax 408-431-4122; http://www.borland.com.
* Centura Software Corp., 1060 Marsh Rd., Menlo Park, CA 94025; 800-444-8782, 415-321-9500, or fax 415-321-5471; http://www.centurasoft.com.
* LBMS Inc., 1800 West Loop S., Houston, TX 77027; 800-325-5267, 713-623-0414, or fax 713-623-4955; http://www.lbms.com.
* Popkin Software and Systems Inc., 11 Park Place, New York, NY 10007; 800-732-5227, 212-571-3434, or fax 212-571-3436; http://www.popkin.com.
* Powersoft Corp., 561 Virginia Rd., Concord, MA 01742; 800-395-3525, 508-287-1500, or fax 508-269-3997; http://www.powersoft.com.
* Rational Software Corp., 2800 San Tomas Expressway, Santa Clara, CA 95051; 800-718-1212, 408-496-3600, or fax 408-496-3636; http://www.rational.com.
* Riverton Software Corp., One Kendall Square, Building 200, Cambridge, MA 02139; 617-588-0500 or fax 617-588-0412; email bgleason@riverton.com.

FIGURE 1


--There are at least four major approaches to the inheritance hierarchy for just the three windows visible in this sample application. Five functional areas that start with very similar "Find" dialogs will take the user to a tab folder window with detail data. These five pairs of windows represent a potentially reusable pattern in which most behavior can be generalized.

FIGURE 2


--A more general solution that adheres to OO guidelines requires that all windows have a single common ancestor. This implementation is not supported in all Windows 4GLs but is always preferred.

FIGURE 3


--This figure shows the same object model as in Figure 2 with delta classes added to allow changes to class library behavior without modifying the class library. This architecture is useful if you purchased your class libraries and would like to upgrade to a later version while also making changes to class library behavior. This architecture is by nature inefficient and consumes large amounts of memory, but it does provide maximum flexibility. It may be overkill for many applications.

FIGURE 4


--A find window ancestor typically has many descendants. All OO 4GLs encourage this architecture, but it is very inefficient if the differences in each find window are small.

FIGURE 5


--An advanced object model with all of the functionality of Figures 2 and 4 combined. The individual find windows are not created from separate classes but are all instances of a single class. The same is true of the tab folder windows. This architecture may be difficult for beginners to implement without support tools; implementing the find windows with this architecture is fairly simple, but implementing the tab folder windows requires quite a bit of design.

Table of Contents - September 1996 | Home Page


Copyright © 1996 Miller Freeman, Inc. ALL RIGHTS RESERVED
Redistribution without permission is prohibited.
Please send questions or comments to mfrank@mfi.com
Updated Friday, September 20, 1996