Designing fields and methods

How to keep fields focused and methods decoupled

1 2 3 Page 2
Page 2 of 3
// In source packet in file coupling/ex1/Liquid.java
class Liquid {
    private static final double FL_OUNCES_PER_ML = 12.0/355.0;
    private static final double ML_PER_FL_OUNCE = 355.0/12.0;
    /**
    * Converts fluid ounces to milliliters
    */
    public static int convertOzToMl(int ounces) {
        double d = ounces * ML_PER_FL_OUNCE;
        d += 0.5;       // Must add .5 because (int) truncates
        return (int) d; // Result now rounded up if fraction >= .5
    }
}

Note that even though the above method makes use of a constant value, the constant value doesn't increase the method's coupling. (This is not only true conceptually, but also in how Java programs are compiled. As mentioned previously in this article, if a class uses a constant, even if it is from another class, its class file gets its own local copy of that constant value.)

To use this method, another method simply passes in the number of ounces and stores the returned number of milliliters:

// In source packet in file coupling/ex1/Liquid.java
class Example1 {
    public static void main(String[] args) {
        int mlFor8Oz = Liquid.convertOzToMl(8);
        int mlFor12Oz = Liquid.convertOzToMl(12);
        int mlFor16Oz = Liquid.convertOzToMl(16);
        System.out.println("Ml for 8 oz is: " + mlFor8Oz);
        System.out.println("Ml for 12 oz is: " + mlFor12Oz);
        System.out.println("Ml for 16 oz is: " + mlFor16Oz);
    }
}

Your aim when you write a utility method, such as convertOzToML(), should be to take input only from parameters and express output only through parameters or a return value or exception. In Figure 2, you can see a graphical depiction of this kind of method.

Figure 2. A good (minimally-coupled) utility method

Figure 2 shows a structure chart, from structured (not object-oriented) design. Although structure charts are not generally useful in an object-oriented design process, they are useful for graphically depicting the input and output to methods. For this reason, I'll be using structure charts in this article to help you visualize the coupling of methods.

A bad utility method

One way to increase the coupling of a utility method (or any other kind of method) is to pass objects that contain input data or are the recipient of output data, when the objects are not vital to the performance of the method. For example, perhaps when you first write convertOzToMl(), you plan always to put its output into a CoffeeCup object, as in:

// In source packet in file coupling/ex2/Example2.java
class Example2 {
    public static void main(String[] args) {
        CoffeeCup cup = new CoffeeCup();
        int amount = Liquid.convertOzToMl(16);
        cup.add(amount);
        //...
    }
}

If so, you might be tempted to write the convertOzToMl() method like this:

// In source packet in file coupling/ex3/Liquid.java
class Liquid {
    private static final double FL_OUNCES_PER_ML = 12.0/355.0;
    private static final double ML_PER_FL_OUNCE = 355.0/12.0;
    /**
    * Converts fluid ounces to milliliters
    */
    public static void convertOzToMl(int ounces, CoffeeCup cup) {
        double d = ounces * ML_PER_FL_OUNCE;
        d += 0.5;
        cup.add((int) d);
    }
}

So you could use it like this:

// In source packet in file coupling/ex3/Example3.java
class Example3 {
    public static void main(String[] args) {
        CoffeeCup cup = new CoffeeCup();
        Liquid.convertOzToMl(16, cup);
        //...
    }
}

The problem here is that convertOzToMl() is now coupled to the CoffeeCup class. This is less flexible than the first version of convertOzToMl(), which returned the milliliters as an int. If later, someone wanted to convert ounces to milliliters for some purpose that didn't involve a coffee cup, they would have to rewrite the method, write a different method, or create a CoffeeCup object just to hold the output.

In Figure 3 you can see a graphical depiction of this kind of method.

Figure 3. A bad utility method

A truly ugly utility method

The worst way to write the convertOzToMl() method (the way that yields the maximum coupling) is to take an input from a public static variable and put the output in another public static variable. A public static (but not final) variable in Java is equivalent in functionality and danger to global variables of C or C++. Here's an example:

// In source packet in file coupling/ex4/Liquid.java
// THIS APPROACH WORKS, BUT MAKES THE CODE HARD TO UNDERSTAND
// AND HARD TO CHANGE
class Liquid {
    private static final double FL_OUNCES_PER_ML = 12.0/355.0;
    private static final double ML_PER_FL_OUNCE = 355.0/12.0;
    /**
    * Converts fluid ounces to milliliters
    */
    public static void convertOzToMl() {
        double d = PurpleZebra.k * ML_PER_FL_OUNCE;
        d += 0.5;
        FlyingSaucer.q = (int) d;
    }
}
// In source packet in file coupling/ex4/FlyingSaucer.java
class FlyingSaucer {
    public static int q;
    //...
}
// In source packet in file coupling/ex4/PurpleZebra.java
class PurpleZebra {
    public static int k;
    //...
}

To use the above version of convertOzToMl(), a programmer would have to do the following:

// In source packet in file coupling/ex4/Example4.java
class Example4 {
    public static void main(String[] args) {
        PurpleZebra.k = 16;
        Liquid.convertOzToMl();
        int mlFor16Oz = FlyingSaucer.q;
        System.out.println("Ml for 16 oz is: " + mlFor16Oz);
    }
}

To use this version of convertOzToMl(), client programmers would have to know a lot about the internal implementation of the method. They would have to know they must put their ounces into the static variable PurpleZebra.k and grab the milliliters out of FlyingSaucer.q. By contrast, the "good" version of convertOzToMl() shown earlier in this article enabled programmers to understand how to use it simply by looking at the method's signature and return type. This "ugly" version, because its signature and return type don't reveal all its inputs and outputs, is harder to understand than the good version.

What's more, someone working on FlyingSaucer, not realizing that q was being used elsewhere, might delete the variable or use it for some other purpose. If q does get used for some other purpose and the program is multithreaded, the value of q could get trampled by a different thread after it is assigned the ounces but before convertOzToMl() gets a chance to use it.

This style of programming yields code that is difficult to change because changes can have unforeseen side-effects. In this example, the convertOzToMl() method has a high degree of coupling. It is coupled to two classes, FlyingSaucer and PurpleZebra, which are required for passing data to and from the method. In Figure 4, you can see a graphical depiction of this kind of programming:

Figure 4. Please don't put spaghetti in your computer

Minimally coupled state-view methods

A minimally coupled state-view method:

  • Uses as input only parameters and the class variables of the class that declares method, plus (if an instance method) the instance variables of the object upon which the method is invoked
  • Expresses its output only through its return value, parameters, by throwing an exception

The input rule could alternatively be put like this: The input to a minimally coupled state-view method can come from anywhere except directly from non-constant class variables declared in other classes.

The isReadyForNextUse() method of the CoffeeCup class shown previously in this article is an example of a minimally coupled state-view method. This method takes input only from the needsWashing instance variable and expresses output only through its return value.

Figure 5 shows a graphical depiction of isReadyForNextUse().

Figure 5. A minimally coupled state-view method

Minimally coupled state-change methods

A minimally coupled state-change method:

  • Uses as input only parameters and the class variables of the class that declares the method, plus (if an instance method) the instance variables of the object upon which the method is invoked
  • Expresses its output only through its return value, parameters, by throwing an exception, through the class variables of the class that declares the method, or (if an instance method) through the instance variables of the object upon which the method is invoked

The input requirements of a minimally coupled state-change method are just like those of a minimally coupled state-view method: the input can come from anywhere except directly from non-constant class variables declared in other classes. Similarly, the output of a minimally coupled state-change method can be expressed in any fashion except by directly modifying data declared as or referenced from class variables declared in other classes.

The add() method of the CoffeeCup class shown below is an example of a minimally coupled state-change method:

// In source packet in file coupling/ex5/CoffeeCup.java
class CoffeeCup {
    private int innerCoffee;
    public void add(int amount) {
        innerCoffee += amount;
    }
    //...
}

The add() method takes as input one parameter, amount, and one instance variable, innerCoffee. It expresses its output by changing the innerCoffee instance variable. Figure 6 shows a graphical depiction of the add() method.

Figure 6. An instance method of the CoffeeCup class

The hidden this reference

Note that in Figure 5, the needsWashing instance variable is shown as input to the isReadyForNextUse() method. In Figure 6, the innerCoffee instance variable is shown being passed down as input and passed back as output. Although the purpose of treating instance variables as input and output to methods is to help you visualize method coupling, this treatment is also representative of how instance methods work in the Java virtual machine.

Instance methods are able to access instance variables because the methods receive a hidden this reference as their first parameter. The Java compiler inserts a reference to this at the beginning of the parameter list for every instance method it compiles. Because an instance method receives a reference to the instance variables, those instance variables can serve as input, output, or both. A hidden this reference is not passed to class methods, which is why you don't need an instance to invoke them and why they can't access any instance variables.

Conclusion

This article covered some very fundamental territory, which can be summarized as three guidelines:

  1. The especially valuable guideline for fields

    Don't assign a special meaning to special values of a field in an effort to represent multiple attributes with the single field. Instead, use a separate field to represent each separate attribute of a class or object.

  2. The constant corollary

    Prefer constants (static final fields) over hard-coded literals (1.5, "Hi there!"), especially if you need to use the same constant value in multiple places.

  3. The minimize-coupling mantra

    Always strive to minimize the coupling of methods: Take as input only data needed by the method; express as output only data produced by the method.

Next month

In next month's Design Techniques I'll continue the mini-series of articles that focus on designing classes and objects. Next month's article, the third of this mini-series, will discuss cohesion, the degree of relatedness between the various functions performed within a method body.

I imagine that some readers will find they already know all about the guidelines presented in this and next month's articles. I hope that such readers will forgive me for being obvious and bear with me while I get through the basics. As I said at the start, I think it's important to reiterate the basics from time to time. After next month's article, I'll be writing about design guidelines that are more specific to Java.

A request for reader participation

Software design is subjective. Your idea of a well-designed program may be your colleague's maintenance nightmare. In light of this fact, I am trying to make this column as interactive as possible.

I encourage your comments, criticisms, suggestions, flames -- all kinds of feedback -- about the material presented in this column. If you disagree with something, or have something to add, please let me know.

You can either enter a comment via the form at the bottom of the article, e-mail me directly (see the e-mail link in my bio below), or participate in a discussion forum devoted to this material.

Bill Venners has been writing software professionally for 12 years. Based in Silicon Valley, he provides software consulting and training services under the name Artima Software Company. Over the years he has developed software for the consumer electronics, education, semiconductor, and life insurance industries. He has programmed in many languages on many platforms: assembly language on various microprocessors, C on Unix, C++ on Windows, Java on the Web. He is author of the book: Inside the Java Virtual Machine, published by McGraw-Hill. The small print: "Designing Fields and Methods" Article Copyright (c) 1998 Bill Venners. All rights reserved.
1 2 3 Page 2
Page 2 of 3