Effective object-oriented design

Revisit what Tony means by effective OO design

I've received quite a bit of feedback on my "Singletons Rule" Q&A. Unfortunately, much of the confusion seems to stem from this sentence:

The difference between using a singleton over a class with static methods boils down to effective object-oriented design.

In that response, I did not adequately describe what I consider "effective object-oriented design." For that oversight, I do apologize. Sometimes I take too much for granted.

In fact, you may have read the emails a reader and I swapped in the February 23 "Letters to the Editor" (scroll down to the "Singletons Rule" heading).

Considering all this, what is effective object-oriented design?

One important facet of effective object-oriented design is encapsulation. Encapsulation hides your classes' inner details from the outside world. Moreover, it protects your objects from improper use by forcing all use to go through the objects' interfaces. Encapsulation also protects objects that use the encapsulated object. If not for encapsulation, the users of your object could become dependent upon the object's implementation. This tight coupling between objects means that the users of your object would break each time you needed to rework the implementation.

Static classes provide encapsulation.

Let's look at a class that has only static methods:

public class TraceClass {
      private static boolean debug;
      public static void writeDebug( boolean d ) {
            debug = d;
      }
      public static void debug( String message ) {
            if( debug ) { // only trace if debug is true
                  System.out.println( "DEBUG: " + message );
            }
      }
      public static void error( String message ) {
            System.out.println( "ERROR: " + message );
      }
      protected TraceClass() {} // do not allow instantiation
}

The TraceClass writes trace messages to the command line. The TraceClass keeps an internally encapsulated state: debug. All interaction with TraceClass must be done through the interface. Indeed, we have encapsulation.

However, there are two other facets to effective object-oriented design: inheritance and polymorphism.

You can write the following:

public class ChildTraceClass extends TraceClass {
      private static boolean error;
      public static void writeError( boolean e ) {
            error = e;
      }
      public static void error( String message ) {
            if( error ) {
                  System.out.println( "ERROR: " + message );
            }
      }
      public static void fatal( String message ) {
            System.out.println( "FATAL: " + message );
      }
      protected ChildTraceClass() {} // do not allow instantiation
}

So you can get a piece of what the inheritance offers. However, there is more to inheritance than simple implementation reuse. Inheritance also allows you to program by difference, and allows for type substitution.

The example above shows inheritance for difference. However, it is impossible to substitute ChildTraceClass for TraceClass. You cannot pass a class as argument to a method or assign it to a variable. You can't register a static as a listener. You can't say method( TraceClass ). This limitation makes type substitutability impossible and limits how you can use a class that only has static methods.

In my opinion, inheritance's most important use is for its ability to define substitutability relationships. Reuse is simply a nice bonus when it happens. However, reuse is not the end goal.

Since we lack the ability to substitute, polymorphism is impossible. However, let's explore a slightly different example. Let's consider a class that has both instance and static class methods. Polymorphism (and overriding in general) fails. In Java, you CANNOT override static methods.

Consider the following hierarchy:

public class Parent {
      public static String getMessage() {
            return "I am the parent";
      }
}

And:

public class Child extends Parent {
      public static String getMessage() {
            return "I am the child";
      }
}

Now run the following main:

      public static void main( String [] args ) {
            Parent p = new Parent();
            Child  c = new Child();
            System.out.println( p.getMessage() );
            System.out.println( c.getMessage() );
            Parent p2 = c; // upcast the child
            System.out.println( p2.getMessage() );
            // if overriding works, you should still
            // see "I am the child."
      }

The first two printouts work as you might expect. You'll see:

I am the parent
I am the child

However, the third printout does not work as expected. Instead, you'll see:

I am the parent

Even though you are actually manipulating a child instance, you see the parent's behavior when you call the static method. Overriding does not work for static if you upcast the instance! With a static you'll get the behavior of whatever class you upcast to. Thus you cannot have polymorphic behavior.

As an exercise, remove the static keyword from Parent and Child. Then rerun the main. You'll see that the third printout results in "I am the child" as expected.

I hope that this clears up any remaining confusion.

Tony Sintes is a principal consultant at BroadVision. A Sun-certified Java 1.1 programmer and Java 2 developer, he has worked with Java since 1997.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies