Avoiding the Pitfalls of Object-Oriented Programming
For more information on business objects, see my article "Visual FoxPro Business Objects" in the June, 1999 issue of FoxPro Advisor magazine.
Inheritance is defined as: "The ability of a subclass to take on the characteristics of the class it's based on. If the characteristics of the parent class change, the subclass on which it is based inherits those characteristics."
Inheritance, while a powerful object-oriented technique (and a VFP "one-up" over Visual Basic) can easily be misused. While it's been proven conclusively that deep levels of class hierarchies make little difference to the speed of a Visual FoxPro application, they can still make an application extremely difficult to understand, debug and maintain! I encountered one project in particular during a project rescue that was a classic example of inheritance gone awry. The hierarchy was so deeply nested and complicated that it could take an entire day to track down and fix a single bug!
Object-oriented "composition" offers an excellent alternative to inheritance. Composition is defined as "Combining multiple objects to achieve more complex functionality". A good example of composition at work is the "Iterator-Visitor" design pattern.
To understand the iterator-visitor design pattern, consider the pattern in the context of a word processing application. For the sake of this example, assume that each word in a word processing document is an object. Given this scenario, what kind of object would you build to spell check the document? Your first solution might be to create an object that iterates through all words in a document and spell checks each word.
You may be happy with this structure until you begin building hyphenation into the word processor. You would soon realize that you need to reuse the iterating functionality that is built into the spell checker. So what do you do? Your first reaction might be to subclass the spell checker object and override its methods to perform hyphenation checking. However, when you override the majority of a parent class' behavior (rather than extending it), you end up with a poor object-oriented design…one that's hard to understand, reuse and maintain. Another alternative is to cut and paste the iteration logic from the spell checker into the hyphenation object…another bad choice for obvious reasons!
A far better solution is to create separate objects that perform one unique set of responsibilities well. You could create an iterator object that knows how to do one thing well-iterate through all words in a word processing document. You could then create a second object that also knows how to do one thing well-spell check words. Separately, these objects may not do very much, but when they are combined or composed together at run time, you can spell check an entire document!
In addition, this architecture now affords plenty of reusability. You can now create a hyphenation object that can also be joined to the iterator object at run time to hyphenate an entire document. Each piece of the Iterator/Visitor can be used separately or composed together at runtime to perform more complex tasks.
Many developers tend to create "Swiss Army Knife" objects that do too many things. Objects may start out lean and well-defined, but as your application development progresses, more properties and methods get added to the object…some of these only slightly related to the original intent of the object. Bloated objects make an application unstable, difficult to understand and debug.
The goal is to create objects with clear, crisp boundaries which produce well-defined interfaces and a well-balanced load of responsibilities. This provides greater reusability, maintainability and optimization.
In the example of the iterator-visitor pattern, the crisp boundaries between the objects afford a high degree of reusability. If you need to iterate through a collection of objects, the iterator does that well and can be reused many times over. The spell check visitor knows how to spell check words and can be instantiated by itself when only checking the spelling of a single word-again a high level of reuse. Well-defined objects ease maintenance by making the location of application logic intuitive. For example, if your application has a bug in the logic that calculates a customer's credit limit, where is the first place you look for the corresponding code? In the Customer business object! It encapsulates all of the application logic pertaining to the customer.
Optimization is also a great reason to create objects that perform one set of responsibilities well…particularly in n-Tier environments. Both Active Server Pages and Microsoft Transaction Server work much better with a greater number of smaller, lightweight objects rather than a few heavyweight, bloated objects!
As you create objects for your applications, remember the concept of one-to-one correspondence between the real world and your application's objects. When deciding which object in your application should handle a particular responsibility, it often comes down to common sense. For example, a customer object should not contain invoicing responsibilities. An inventory object should not handle invoicing responsibilities. Rather, objects should encapsulate only those responsibilities that are reasonably associated with what the object represents.
Also, keep in mind that when you are creating a business object, you are creating an API that you or another developer will have to use in the future. To get a better idea of the kind of interfaces you should create, look at applications such as Microsoft Office that act as automation servers and expose several internal "business" objects to the developer. Notice how the different objects correlate to entities in the real world-a spreadsheet, a word processing document, an e-mail, a contact, etc. Look at the properties and methods that comprise the interface of these objects and you'll see how you may (or in some case may not) want to create your own business objects.
One of the main complaints I hear from developers that are moving to Visual FoxPro is that it's difficult to figure out where all the code is. To "help" themselves in this regard, many developers make the mistake of placing too much logic into a single, bloated object method. However, this approach creates the following problems:
Take for example an "Invoice" object that contains a method called "CalcInvoiceTotal". This method contains all the logic to 1) Add Invoice Items 2) Calculate Discounts 3) Calculate Tax and 4) Calculate Invoice Total:
Although you might like having all of this logic in a single method, what happens when you only need to calculate tax on an invoice? For that matter, what if you need to call each of the four chunks of logic individually? You have two options…the first is to add four parameters to the CalcInvoiceTotal method that specify whether or not a particular part of the process should be executed. For example:
*--- Add Invoice Items IF tlAddItems … ENDIF
*--- Calculate Discounts IF tlCalcDiscounts … ENDIF
*--- Calculate Tax IF tlCalcTax … ENDIF
*--- Calculate Invoice Total IF tlCalcTotal … ENDIF
This option produces a very poor design. It makes the interface to your objects difficult to understand, document and maintain. Although you can now reuse a single chunk of logic within the method, the method is cluttered by parameters that you must now document and remember whenever calling this method! We have effectively created "spaghetti" object-oriented code.
Another problem with this option is that it makes the code within the method very difficult to extend. For example, if you create a subclass of the Invoices object with the intent to extend or enhance only the tax calculation logic, how would you do it? You would have to copy all the code from the parent class, paste it into the subclass then modify the tax calculation logic. This again is an extremely poor design that creates a heavy debugging and maintenance burden. If there's a bug to be fixed or an enhancement to be made in the parent method, you now have to fix the same bug or make the same enhancement to the subclass method.
It's a far better design to create separate methods that can be called individually. For example:
In the case of reusability, this allows you to call each method individually without having to remember several parameters. In the case of extensibility, this design makes it very easy to extend a single portion of this logic in a subclass. Using the example of enhancing the tax calculation, you can easily create a subclass of the Invoices object, and place the following code in its CalcTax method:
In addition to breaking out complex methods into smaller methods, it's a good idea to provide a single method that can be called to perform all the logic necessary for a particular task. For example, it's still a good idea to provide a single CalcInvoiceTotal method that internally calls the other four methods:
This provides a higher level interface so developers using your objects do not need to remember all the methods that need to be called (and the proper order of methods) in order to calculate an invoice total. At the same time each part of the process can still be called individually.
A good example of logic that should always be broken out into a separate method is the instantiation of objects. A method containing logic that creates an object is called a "Factory method". Factory methods make it very easy for developers to create subclasses that override the method and instantiate a different object than specified in the parent class.
Limiting how much one object knows about the internal structure of another object produces "loose coupling" between the objects. When an object's internal data structure and code is hidden from the outside world, it is said to be "encapsulated". Encapsulation isolates the internal complexity of an object's operation from the rest of the application.
Some benefits of loose coupling are:
Although many developers understand the concept of loose coupling, the temptation arises to tightly couple objects and break encapsulation-especially when under deadline.
In order to understand the concept and the benefits of loose coupling, consider a real-world problem I encountered and solved using these principles.
The problem presented itself when incorporating Visual FoxPro's "Trace-Aware" timer class into our commercial Visual FoxPro framework. Trace-aware timers automatically shut themselves down when a VFP debug window is opened, allowing you to debug without constant interruption from the timer. However, if there are several timers in a particular application, how do you restart the timers when you've finished debugging?
The first part of the solution was to create a "Timer Manager" class that is responsible for keeping track of all application timers. As each application timer instantiates, it registers with the Timer Manager by passing a reference to itself to the Timer Manager's RegisterTimer method. This allows timers to be easily restarted after finishing a debug session, by sending a single message to the Timer Manager's RestartTimers method.
However, this solution presented a problem of its own. If a timer resides on a Visual FoxPro form, and the user tries to close the form, VFP will not release it. This is because the Timer Manager still has a reference to the timer on the form and VFP will not release a form when there is an outstanding reference to an object on that form. This situation is called a "dangling object reference". How can this problem be solved? The solution lies in releasing the object reference to the timer before the form is destroyed.
At first, it may seem to make sense to place code in the Timer's Destroy method that releases the object reference from the Timer Manager. However, this option will not work because Visual FoxPro never calls the Timer's Destroy method due to the dangling object reference.
Another option that does work, is to place code in the form's Destroy method that releases any timers contained within the form. However, this solution is unsatisfactory because every time a developer drops a timer on a form, they need to remember to place the "release" code in the form's Destroy method…thus producing tight coupling between the form and the objects that reside on the form.
What's needed in this situation is the ability to raise a "Destroy" event. This lets any interested objects on the form know that the form is about to be destroyed and allows them to do something first…for example, allowing timers to unregister themselves from the Timer Manager.
Visual Basic has a built-in "RAISE EVENTS" command that allows developers to raise custom events. This is essentially a generic broadcast that is sent out to any object that may be "listening". You can specify that an object receive and respond to a custom Visual Basic event by using the WITH EVENTS command.
As you probably know, Visual FoxPro does not provide the ability to raise custom events…or does it? Here is the cliff hanger…attend this session and see how two new features of Visual FoxPro allow you to raise and respond to custom events!
A common mistake made by developers is to sacrifice speed for what they believe is a more "OOP-ish" solution. However, if you deliver an application that runs s-l-o-w it will be difficult to win over your end users (or your project sponsor!) on the benefits of object-orientation. In reality, the best OOP solution is both object-oriented and optimized!
A good example of this is found in the implementation of Business Objects in an application. In Visual FoxPro, you can drop a business object directly on a form. As mentioned earlier, business objects act as a conduit for data manipulation and, as such, can load Visual FoxPro cursors into a form's Data Session.
Some developers feel you should not bind a user interface control's ControlSource directly to a Visual FoxPro cursor that has been loaded into a form's data session by a business object. Rather, they recommend that field values from a business object cursor should be copied to corresponding properties of a business object, and ControlSources should be tied to these properties. For example, a simple customer cursor would translate into an object as follows:
VFP Customer Cursor
VFP Business Object Properties
[952 Rockledge Dr.]
The thinking behind this recommendation is that the structure is more scalable in the following scenarios:
Moving to a three-tier architecture (where business objects are physically moved to a different computer and accessed via DCOM)
Reusing business objects in non-Visual FoxPro environments
The problem with this solution is this: Visual FoxPro works best with data served up as cursors, not as properties of an object. What happens when you want to display a few hundred records in a grid? You need to manually create a cursor by getting the "next" business object record over and over again…not a very optimized solution!
Again, there is a solution to this problem that is both object-oriented and optimized. It involves creating objects that are flexible in their architecture and can adapt to their host environment.
First of all, when moving business objects to a separate machine, in a true three-tier distributed architecture, you don't want to move the entire business object. The reason for this is two-fold. First of all, in order to communicate with a distributed object, you must use Microsoft's Distributed COM, and DCOM is very slow! A call to a remote object via DCOM can be a few thousand times slower than a call to an object running in the same process! If you move the entire business object to another machine, your application will run very slowly.
A better solution is to divide the business object into two pieces-a user-interface centric piece and a data-centric piece. In this architecture, you place the processing closest to where it needs to be. The UI-centric portion of the business object can be dropped directly on a Visual FoxPro form and can its properties and methods can be accessed directly by the object. The data-centric portion of the business object can be distributed on a remote machine, where it is closer to the data. The UI-centric portion of the business object can then communicate with the data-centric portion of the business object whenever it needs to manipulate data or run a data-centric, data crunching process. All of this happens without the knowledge of the user interface. As far as it knows, the entire business object resides in tier 1.
The second consideration is the format in which data is presented by the business object. Rather than hard-coding business objects to present a single data format, they can be designed to present data in the format best suited to the current client. This may be in the form of a Visual FoxPro cursor, an ADO recordset, XML, HTML, business object properties and so on.
Although this rule may be difficult to grasp at first, once you understand the concept, it is key to building applications that are flexible, extensible and easy to maintain.
This concept is more easily understood in the context of a real-world scenario. In our commercial Framework, we have a database manager class that is responsible for managing all application database containers. It has the ability to track sets of database containers that are used simultaneously and can dynamically switch between these sets at run time.
Some developers using our Framework like to store the database information in the Windows Registry. Other developers prefer to store the information in a Visual FoxPro table. This aspect of the database manager is not really key to its functionality, but is really a detail of its implementation.
How can you design a solution so the desired functionality (reading/writing the Windows Registry or a VFP table) is used consistently throughout your application?
Solution 1 entails programming directly to an implementation. Throughout your application, you could place CASE statements that check an application setting to determine whether to use the Windows Registry or the VFP table for database settings.
This is a poor solution because it's not very extensible. What happens if you want to store database settings to a third type of file…perhaps a text file. Given this structure, you would have to find every CASE statement in your application and add a third condition, creating a maintenance nightmare!
Solution 2 is a far better solution…programming to an Interface. An interface does not refer to user interface. An interface is the complete set of requests that can be sent to an object, or in other words, all of an object's methods, their parameters and return values. An interface allows you to define a family of classes that can be swapped dynamically at run time.
The first step in programming to an interface is to create an abstract base class from which all other database manager classes can be subclassed.
An abstract class is one whose main purpose is to define a common interface for its subclasses. As shown in Figure 1, the class CDatabaseMgrBase is an abstract interface that defines the following common methods for its subclasses: IsDataBaseSetup, SETDBCPath, GetDefaultDatabase, GetFullyQualified-Database, SetDBCPath.
The CDatabaseMgrRegistry and CDatabaseMgrTable classes inherit this interface from CdatabaseMgrBase. However, each of these subclasses contain different implementation code in these inherited methods. The methods in the CDatabaseMgrRegistry class read and write to the Windows Registry. The methods in the CDatabaseMgrTable class read and write to a Visual FoxPro table.
After these classes have been created, the next step is to select one of these classes to be instantiated as the database manager at run time. This can be done once at application startup by checking the value of an application property. Based on the value of the property, one or the other subclass can be instantiated at run time. Here's an example of what this code might look like:
From this point on, the rest of the application does not need to know which specific subclass was instantiated at startup. They can simply refer generically to the Database Manager interface, without worrying about the implementation.
For example, the following code gets the name of the application's "default" database:
At this level, we don't care about the implementation-where the database information is stored. We're simply interested in retrieving the default database name.
This solution is far more extensible than the first solution. If you decide to store settings in a different type of file, such as a text file, here's all you have to do:
Whenever there is more than one reasonable way to implement a particular API, and those different implementations can have useful differences, consider adding an abstract class at the top of the class structure!
One of the first questions I ask when taking on a project rescue is: "May I see your analysis and design documentation?" Invariably, the answer is: "We didn't have time for that". Developers may find out too late that the right kind and right amount of design and analysis can actually reduce the time to market and decrease cost over the life of the project. Developers who skip the design and analysis phase find themselves spending far more time reworking, refactoring, backtracking and duplicating efforts.
In November, 1997 the Unified Modeling Language was officially adopted by the Object Management Group (OMG) board of directors as the standard for documenting the artifacts of an object-oriented application. Companies such as Microsoft, Oracle, Rational, IBM and Hewlett-Packard were part of a UML partners consortium that was formed to make the UML a solid, well-defined standard.
I've discovered an interesting indicator of the broad acceptance of the UML. When I travel to different cities around the country, I often check with the local university book store, and invariably, they have UML textbooks that are used in their school's curriculum.
The Unified Process, although not officially part of the UML, helps you understand the UML in the context of a software development process. The UML defines the syntax and type of diagrams used in the design and analysis process. The Unified Process defines the steps to take to turn a system specification into an object-oriented application.
On the other end of the spectrum is analysis paralysis! You can get bogged down in overly detailed diagrams of an entire application. As one OOP guru, Ward Cunningham, put it:
"Carefully selected and well-written memos can easily substitute for traditional comprehensive design documentation. The latter rarely shines except in isolated spots. Elevate those spots…and forget about the rest."
For more information, see my articles on the UML in the January and February 1999 issues of FoxPro Advisor.