Back to Thoughts On Technology Page | Back to Peter's Home Page | Back to Family Home Page



Object Oriented Best Practices
Peter Rose 01/23/2005


ABSTRACT

OVERALL CODE DEVELOPMENT CONSIDERATIONS

OBJECT POPULATION BY REFERENCE

CONDITIONAL LOGIC NESTING

CASCADING METHOD CALLS

ACCESSING AGGREGATE OBJECT PROPERTIES

KEEP THINGS S-I-M-P-L-E!





Abstract
There are a number of well-established best practices for writing object-oriented systems in Java. This paper describes some of those.

Overall Code Development Considerations
I have had great success in enhancing and maintaining applications where I have rigorously adhered to the following coding guidelines which tend to be more specific than overall coding “standards”.

  1. Object encapsulation is at the forefront of consideration.
  2. Use Interfaces to create object contracts
  3. Follow descriptive naming conventions for all classes and methods.
  4. If an object contains more than 3 public methods (other than accessor methods) then it is a sign that the object is not sufficiently encapsulated, particularly controller classes which I many times limit to just 1 public method.
  5. Refrain from passing raw data into methods. Rather favor objects as parameters.
  6. Limit method parameters to 3 or less unless there is compelling reason for more. Most times a method that needs more parameters is not encapsulated and needs to be broken up into other methods.
  7. Limit method body text to what is easily seen in one screen.
  8. Limit each method to one specific discrete operation regardless of how small, even if the operation is not called from multiple other places in the class.
  9. Provide verbose and detailed JavaDoc comments no matter how trivial the method.
  10. JavaDoc all method parameters, returns, and exceptions.
  11. Do not nest conditional logic more than 2 levels; more indicates the need to break out into another method call.
  12. Consolidate all class functionality (i.e. business purpose and process) in one main public method which calls out to other methods in the class to do the work.
  13. Do not nest method calls more than 2 layers deeper then from the main public method (i.e. main public calls method1 which in turn calls method2 - but method2 is not allowed to cascade further).
  14. Try not to go more than 3 levels of inheritance before getting to your concrete class.
  15. Rather than using inheritance hierarchies to facilitate business solution responsibility, modern Object Oriented Design technology suggests combining aggregation (has a) with composition (is a type of) within inheritance hierarchies. For example, pure inheritance might have an ancestor class Person with an Employee descendant which might in turn subclass an HourlyWorker. But HourlyWorker "is a type of" Employee, not an "is a" Employee. Thus, Person will extend Employee but Employee will have an aggregate abstract EmployeeType which will then extend HourlyWorker, SeasonalWorker, etc. as descendants. Deep inheritance hierarchies tend to be quite bloated and clog up the pipe with excess object overhead and referencing through the stack. They can also be quite difficult to understand from a maintenance perspective with later developers coming into the system.

Object Population By Reference
A method that shows no return but where a method parameter is changed by reference back through the calling stack is fraught with problems.


public void findAllTicketsById(int aId, Ticket aTicket) throws DBException {
	--- more ---
	aTicket.setCurrentStatus(flightStatus);
	--- more ---
} //endmethod: findAllTicketsById

It is a confusing code construct to type a method as void and change a parameter by reference in that method. If you need to pass an object in and make changes to it, then the correct structure is to return it as part of the method signature as follows:


public Ticket findAllTicketsById(int aId, Ticket aTicket) throws DBException {
	--- more ---
	aTicket.setCurrentStatus(flightStatus);
	--- more ---
	return( aTicket );
} //endmethod: findAllTicketsById

Conditional Logic Nesting
In the Item.java file’s validate(…) method, for example, there are 408 lines of code between the try and the catch and much of this code has conditional logic nesting 8 levels deep (as shown in the small code snippet below)!


                        } // - level 9
                     } //  - level 8
                  } //  - level 7
               } //  - level 6
            } //  - level 5
         } //  - level 4
      } //  - level 3
   } // - level 2
} else {

Code written in this manner stands a far higher likelihood of not meeting the objectives of the intended business process flow it is supposed to solve. And then there is the issue of enhancement and maintenance… This is not to say that the above is an example of "bad" code, but rather that the author was pressed for time and could not go back and refactor. How to avoid this - because who has time to refactor? Never code more than you have to in order to express your solution process. This might mean just the outline of the conditional statements you know you will have to go through to solve your problem. Once you get a couple of levels deep, insert a method call in the current conditional to return a boolean or object rather than continuing to nest. You just need to refactor as you go. It's as simple and as difficult as that.

Cascading Method Calls
What really should be done to resolve the issue of Conditional Logic Nesting (as shown in that section) is to use a Façade pattern.

The Façade pattern is usually thought of as being used at the object level, but it can also be applied in the code at the method level. The Façade is a main public method the user calls which then carries out all of the work of the class. This is what I was referring to in my general coding guidelines in having an object’s business logic controlled from one main public method which describes and resolves the entire solution by calling out to other methods in the class, none of which is nested more than 2 levels down. An example of this type of construction is shown below.


//========================================================================
/**
 * This is the main driver method of the class which is called from the
 * fdmadmin.jsp page through the FDMAdminRequestHandler. It acts on the
 * value of the action button the user
 * selelected.
 */
//========================================================================
public void validate(FDMBean pfdmbean) {
        fdmbean = pfdmbean;
        vErrors = new Vector();
        vdests  = convertStringOfCodesToVector( destinations );

        if( action.equals("clear") || action.equals("") ) {
                //-- Don't do anything; already been taken care of in the request handler.
        } else if( action.equals("commit") ) {
                doActionVerify();
                if( vErrors.size() == 0 ) {
                        vErrors.add("The file has been verified and found correct.");
                        doActionCommitRecord();
                        clearFields();
                } //endif
        } else if( action.equals("verify") ) {
                doActionVerify();
                if( vErrors.size() == 0 ) {
                        vErrors.add("The file has been verified and found correct.");
                } //endif
        } else {
                if( okData(airportCode, airportName, isInterline, vdests) ) {
                        if( action.equals("add") ) {
                                doActionAddNewRecord();
                        } else if( action.equals("delete") ) {
                                doActionDeleteRecord();
                        } else if( action.equals("update") ) {
                                doActionUpdateRecord();
                        } else {
                                xdebug("WARNING: Invalid action command found from fdadmin.jsp.");
                        } //endif
                } else {
                        //-- Do nothing as vErrors will display on fdmadmin.jsp
                } //endif
        } //endif
} //endmethod: validate

And as an example of one of these called service methods:


//========================================================================
/**
 * Called from validate: add a new record.
 */
//========================================================================
protected void doActionAddNewRecord() {
        if( fdmbean.containsOriginCode(airportCode) ) {
                vErrors.add("Origin code " + airportCode + " for airport name: " +
                        airportName + " is already listed on file. No new entry created.");
        } else {
                if(!fdmbean.addMatrixRecord(airportCode,airportName,isInterline,destinations) ) {
                        airportCode     = "";
                        vErrors.add("Input Error: No entry was able to be made for: " +
                                 airportCode + " for airport name: " + airportName);
                } //endif
        } //endif
} //endmethod: doActionAddNewRecord

Accessing Aggregate Object Properties
In keeping with the OO principal of building loosely coupled objects, aggregate object properties should be accessed via a call to their containing object vs. using an accessor to get the aggregate and then using that object’s accessors to get the property value.

For example, if I wanted to add a delete status to the Ticket.java class and was not sure of how much delete status information I would need, I would create a TicketDeleteStatus.java class and make it an object property of Ticket. TicketDeleteStatus might have 2 initial properties statusId and statusDescr accessed via accessors as any object would. The object graph would appear as follows:


Ticket
	TicketDeleteStatus

However, I would not want to access the statusDescr field in the following manner as it too tightly couples the two objects:


String theDescr = ticket.getTicketDeleteStatus().getStatusDescr();

The implication of this type of construction is that I have to have full knowledge of the object graph of Ticket to get aggregate information. A better solution is to call the accessor on the container and let it figure out where and how to get the information as follows:


String theDescr = ticket.getStatusDescr();

The Ticket object then does what it needs to do to determine the result to return; the calling object does not need to be troubled with the plumbing details.

In addition to eliminating coupling between these objects, it also prevents a potential maintenance nightmare. Let’s say I needed this description String in 150 places throughout my application. Things are fine until one day it becomes evident that the statusDescr property needs to be broken out into numerous different elements. This calls for a new class: TicketDeleteStatusDescription with properties: statusDescr (as is currently used), statusLongDescr, statusDropDownListDisplayText, and statusDefaultText. TicketDeleteStatusDescription becomes an aggregate of TicketDeleteStatus so that our new aggregation relationship becomes:


Ticket
	TicketDeleteStatus
    	TicketDeleteStatusDescription

Now, all 150 calls for the statusDescr property must all be changed from the current form of:


String theDescr = ticket.getTicketDeleteStatus().getStatusDescr();

to reference it in this manner:


String theDescr = ticket.getTicketDeleteStatus().getTicketDeleteStatusDescription().getStatusDescr();

By encapsulating all of this within Ticket, however, I only need to make one change in the Ticket class. The Ticket class’s getStatusDescr() method – which previously just accessed its deleteStatusDescr property – now needs to be changed to call the TicketDeleteStatusDescription class’s getStatusDescr() method. Thus, the call Ticket.getStatusDescr() used in 150 places can remain as is.

Keep Things S-I-M-P-L-E!
From Rusty Herald's Cafe au Lait Java Quote of the day - exactly what we need to keep in mind in our own efforts!
http://www.ibiblio.org/javafaq/

Quote of the Day

...complexity leads to disaster. Your application should be built around simple constructs and understandable layers, which combine to perform complex tasks. The code itself, however, should avoid complexity at every stage. This is much easier to say than to do, though, since many programmers are afraid of missing important pieces, or of oversimplifying.

You don't need to be afraid of doing something too simply if you embrace change in your application. The ability to fearlessly embrace change is based on good testing practices. If your code has thorough, automated, repeatable tests, then making changes to the code loses much of its anxiety. As changes are introduced, the tests will tell you whether or not you are breaking something important. Automated testing gives you a safety net, allowing you to examine simple solutions first, and to change them over time without fear.

--Justin Gehtland Read the rest in ONJava.com: Better, Faster, Lighter Programming in .NET and Java http://www.onjava.com/pub/a/onjava/2004/07/14/BFLJava.html


Back to Thoughts On Technology Page | Back to Peter's Home Page | Back to Family Home Page