Session E-PIT

Avoiding the Pitfalls of Object-Oriented Programming

Kevin McNeish
Oak Leaf Enterprises


Contents

Learn first-hand from an experienced developer who's been involved in numerous Visual FoxPro "Project Rescues" how to avoid the pitfalls of object-oriented programming. Save yourself time, aggravation and cost by learning how to sidestep these pitfalls. Learn to avoid pitfalls in: Analysis and Design, Coding, Classes and Objects, Reusability and Implementation. This session is an excellent precursor to the Advanced Business Objects.

As an added benefit, you'll also learn how to create custom Events in Visual FoxPro!

Pitfall 1: Creating Applications that are Not Scalable

The vast majority of developers I meet have expectations that their applications eventually need to scale up to a more complex architecture such as client-server, a distributed three-tier model or, the one I hear most frequently, be accessible via the Internet.

If an application is not structured correctly, scaling upwards may mean a complete rewrite of an application. Fortunately, there is much that can be done to ensure that an application is easily scalable. Where you place your application code has considerable bearing on its scalability and its reusability.

Prevention: Place Application Code for Maximum Scalability and Reusabiltiy

Consider first the issue of reusability. There are a few different levels of reusability to think about when designing an application. Often, developers think on the application-reuse level. They create methods, procedures and functions that can be reused within single or multiple Visual FoxPro applications.

However, there's a bigger picture to consider-reusing your application logic from non-Visual FoxPro development tools such as C++, Delphi, Visual Basic, Java, as well as software applications such as Microsoft Office.

Starting with version 5.0, Visual FoxPro allows the creation of ActiveX Automation Servers (COM Objects) from VFP classes. This gives tools and applications that know how to talk to COM objects (like those listed above) the ability to access Visual FoxPro applications.

However, many developers make the mistake of placing the majority of their application code in the user interface, similar to the way it was done in FoxPro 2.x. However, since COM objects cannot contain any user interface elements, application logic placed in the user interface cannot be accessed via COM.

Rather than placing code in the user interface, you can create "business objects" that contain the majority of your application logic. These objects can be marked as OLE Public (COM objects) and compiled into an ActiveX Automation Server.

A business object is a high-level, abstract object that represents a person, place, event or business process. For example, a point-of-sale software application may contain objects such as: Customer, Invoice, Payment, Inventory, Employee, and so on. Each of these business objects represents a real-world entity and contains all of the application logic pertaining to these entities. For example, a Customer business object may contain properties and methods that allow it to "know" a Customer's address, credit limit, outstanding invoices and payment history. An Employee business object may contain properties and methods that allow it to know an employee's contact information, employment history and performance.

You can think of business objects as the nouns in sentences that describe your system. The adjectives that qualify each noun translate into object attributes, the verbs specifying actions between nouns translate into object operations, or methods. By mapping concepts from natural language sentences to objects, developers benefit from more accurate specifications, easier maintenance and software reuse.

Many VFP developers have become proficient at subclassing and reusing user interface objects. However, if you're not currently using business objects in your applications, you're missing out on one of the greatest benefits of VFP's object-oriented capabilities-the ability to reuse your application logic both inside and outside of Visual FoxPro.

Scalability

Visual FoxPro COM objects not only provide reusability, they also offer scalability. One of the most interesting uses of VFP COM business objects is the ability to access them from Active Server Pages. You may have been asked (or will be asked <g>) if your applications can be accessed via the Internet (specifically from a thin HTML

client). If you've placed the majority of your application logic in the user interface, the answer is "no"-not without a rewrite. If you've placed the majority of your application logic in business objects, the answer is probably "yes"-and with minimal extra effort.

Microsoft's Active Server Pages can instantiate Visual FoxPro COM objects. So, if the majority of your application logic resides in business objects, your application can be accessed via the Internet.

In addition to access via the Internet, using business objects is also a big step towards migrating to client-server. Business objects act as a conduit for data manipulation and as such, shield the user interface from having to directly access application data. This structure allows you to easily change the data source from Visual FoxPro to client-server data (e.g. SQL Server, Oracle) without changing the user interface or the majority of your application logic!

Business objects are also a big step towards scaling to a three-tier architecture. Business objects can be physically moved to a second tier application server and accessed via DCOM (Distributed COM) creating a true distributed architecture.

Understanding COM

COM (Component Object Model) is a specification created by Microsoft that provides a standard that components and clients can follow to ensure that they can operate together. COM components consist of executable code and are distributed as either executables or dynamic link libraries (DLLs).

 

COM components are language independent. As such, they can be created using a variety of tools such as Visual FoxPro, Visual Basic, C++, C, Java and Pascal. In turn, these same tools can also use COM components. One of the most common examples of COM components are Active-X controls. These controls can be created in a variety of languages and in turn be used by many tools.

 

For more information on business objects, see my article "Visual FoxPro Business Objects" in the June, 1999 issue of FoxPro Advisor magazine.

Pitfall 2: Using Inheritance Badly

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!

Prevention: Favor Composition Over Inheritance

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.

Pitfall 3: Creating Bloated Objects

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.

Prevention: Create Objects that Handle One Set of Responsibilities Well

A good visual depiction of a well-designed object is that of a hard, solid sphere with  several access ports that require standard connectors

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.

Pitfall 4: Creating Bloated Object Methods

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:

  • It greatly limits the reusability of application logic
  • It makes methods difficult to extend
  • It creates "spaghetti" object-oriented code

Reusability

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:

    CalcInvoiceTotal()
    *--- Add Invoice Items
    *--- Calculate Discounts
    *--- Calculate Tax
    *--- 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:

    CalcInvoiceTotal()
    LPARAMETERS tlAddItems, tlCalcDiscounts, tlCalcTax, tlCalcTotal
     
    *--- 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.

Extensibility

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.

Prevention: Keep Object Methods Small and Well-Defined

It's a far better design to create separate methods that can be called individually. For example:

    AddItems()
    CalcDiscounts()
    CalcTax()
    CalcTotal()

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:

    lnRetVal = DODEFAULT()
    <Additional Logic>
    RETURN lnRetVal

Having it All

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:

    CalcInvoiceTotal()
    This.AddItems()
    This.CalcDiscounts()
    This.CalcTax()
    This.CalcTotal()

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.

Factory methods

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.

Pitfall 5: Creating Strong Dependencies Between Objects

Prevention: Maintain Loose Coupling Between Objects

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:

  • It eases maintenance…changing how one object works doesn't require changing other objects.
  • It reduces dependencies in an application allowing greater reuse of objects.

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.

Solution 1: Place Code in the Timer's Destroy Method

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.

Solution 2: Place Code in the Form's Destroy Method

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.

Solution 3: Create a Custom Visual FoxPro Event!

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!

PitFall 6: Sacrificing Speed for OOP "Technique"

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!

Prevention: Realize the best 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

cName

[Oak Leaf]

cAddress

[952 Rockledge Dr.]

cCity

[Charlottesville]

cState

[VA]

cZip

[22903]

 

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!

The Best of Both Worlds!

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.

Pitfall 7: Programming to an Implementation

Prevention: Program to an Interface

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?

Scenario 1: CASE Statement - Programming to an Implementation

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!

Figure 1 Programming to an Interface - The abstract CDatabaseMgrBase class defines a common interface for its subclasses.

Scenario 2: Programming to an Interface

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:

    goApp.oDataBaseMgr = CREATEOBJECT(goApp.cDatabaseMgrClass)

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:

    lcDefaultDBC = goApp.oDataBaseMgr.GetDefaultDatabase()

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:

  1. Create a new subclass from CDatabaseMgrBase.
  2. Add implementation code to its methods.
  3. Set the application object's CDatabasemgrClass property to the name of the class.

When Should You Add Abstract Classes?

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!

Pitfall 8: Underestimating the Need for Analysis & Design

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.

Prevention: Learn the Unified Modeling Language and Unified Process

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.

Avoid Analysis Paralysis

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.