Learn Object-Oriented Java the Hard Way

Exercise 7: Encapsulation and Automated Testing

The OOP part of this exercise isn’t any more difficult than the last exercise. Two fields are changed by a mutator method and accessed (but not changed) by an accessor method.

But in the driver… oh, you’ll see.

SquareRootFinder.java
 1 public class SquareRootFinder {
 2 
 3     double n;
 4     int iterations;
 5 
 6     public void setNumber( double number ) {
 7         n = number;
 8         iterations = 7;
 9         if ( n < 10 )
10             iterations++;
11     }
12 
13     public double getRoot() {
14         if ( n <  0 ) return Double.NaN;
15         if ( n == 0 ) return 0;
16         double x = n/4;
17         for ( int i=0; i<iterations; i++ ) {
18             x = (x+(n/x))/2.0;
19         }
20         return x;
21     }
22 }

So, there’s nothing new to see here. The SquareRootFinder class has two instance variables (n and iterations). The setNumber() method allows the user of the class to pass in a value that will be copied into the field n.

I guess I should mention that NaN stands for “not a number”. It’s a special value that you sometimes get in Java when you try to do something undefined like divide zero by zero or take the square root of a negative.

Then the getRoot() method does some complicated calculations to compute an estimate of the square root of that number. Does it work? Yes. How does it work? That’s the thing about programming. Sometimes you won’t know.

Go ahead and type in the driver code, then we’ll continue this thought.

SquareRootDriver.java
 1 import java.util.Scanner;
 2 
 3 public class SquareRootDriver {
 4     public static void main( String[] args ) {
 5         Scanner keyboard = new Scanner(System.in);
 6         double n;
 7 
 8         SquareRootFinder sqrt = new SquareRootFinder();
 9 
10         do {
11             System.out.print("Enter a number (or <=0 to quit): ");
12             n = keyboard.nextDouble();
13 
14             if ( n > 0 ) {
15                 sqrt.setNumber(n);
16                 System.out.println( sqrt.getRoot() );
17             }
18         } while ( n > 0 );
19     }
20 }

What You Should See

I know that the square root of 4 is 2. (2.0 when it’s a double.) I know that the square root of 2 is 1.414-something. I check it on my calculator on my phone and it gives me “1.4142135624”, which fits with what the driver gives me. I can type in a couple of other numbers and check them by hand and then say “uh, close enough”, but how do I know?

Why does the getRoot() method start out x with n/4? Why is iterations set to 7 inside setNumber()? Why not 5 or 6 or 70? Why don’t we let the user of the class pass in a value for iterations like we do for n?

The answer to all these questions is that sometimes “the user of the class” isn’t the same person as “the creator of the class” and sometimes the user doesn’t have the training and has better things to do or would probably mess these decisions up anyway.

For most programming tasks, there is more than one person involved. It is often better to let a single person say “here is an object, you use it in this way: put in a number here and the answer will come out here.” This is a form of information hiding called “encapsulation”, and it is one of the important concepts in object-oriented programming.

In encapsulation, an object has fields and forces the user of the object to use the methods provided instead of messing with the variables directly. In this example, SquareRootFinder allows the user of the class to pass in a value for n through the setNumber() method but does not allow them to pass in a value for iterations.

Make sense?

Okay, so how does the person who created the class know what value for iterations is “right”? I tested it. Like, a lot. Like, not just “type in a few numbers on the calculator and compare”, but like so:

SquareRootTester.java
 1 public class SquareRootTester {
 2     public static void main( String[] args ) {
 3 
 4         SquareRootFinder sqrt = new SquareRootFinder();
 5 
 6         double max = 0, maxN = 0;
 7         double fakeroot, realroot, diff;
 8 
 9         System.out.print("Testing square root algorithm... ");
10         for ( double n = 0; n<=2000; n += 0.01 ) {
11             sqrt.setNumber(n);
12             fakeroot = sqrt.getRoot();
13             realroot = Math.sqrt(n);
14             diff = Math.abs( fakeroot - realroot );
15             if ( diff > max ) {
16                 max = diff;
17                 maxN = n;
18             }
19         }
20 
21         if ( max > 0.000001 ) {
22             System.out.println("FAIL");
23             System.out.println("Worst difference was " + max + " for " + maxN );
24         }
25         else
26             System.out.println("PASS");
27     }
28 }

What You Would See If You Ran the Tester

In my tester program, I compare the output of the getRoot() method with the “real” square root as computed by Java’s built-in Math.sqrt(). I test every number from 0 to 2000, in increments of 0.01. For each number, I find the absolute difference between “my” square root and the “real” square root, and if the worst difference is more than 0.000001, then I throw an error.

Running this program over and over allowed me to test out different things. The variable x is my initial estimate of the square root; I had initially set x to n, but that starts to get too inaccurate as n gets bigger. Starting x at n/2 is better, but then very small values of n get inaccurate.

The best compromise I found was to start with a guess of n/4, which gets me close enough within seven iterations for every number in the range. Except for values of n between 0 and 1 (where \sqrt{n} > n). I eventually gave up and just decided to give small numbers one extra iteration to compensate for my poor initial estimate in those cases.

Often (for well-designed / well-managed software, anyway) the person who creates a class or someone else on the team will design a “test suite” for that class. For example, SQLite (a database that can be embedded into other software) is famous for being very well tested. Quoting from “How SQLite Is Tested”:

The reliability and robustness of SQLite is achieved in part by thorough and careful testing.

As of version 3.8.10, the SQLite library consists of approximately 94.2 KSLOC of C code. (KSLOC means thousands of “Source Lines Of Code” or, in other words, lines of code excluding blank lines and comments.) By comparison, the project has 971 times as much test code and test scripts - 91515.5 KSLOC.

Whenever they fix bugs or make improvements, the maintainers of SQLite run the full test suite to make sure they didn’t accidentally break anything else! This is a good idea, and the kind of modular design that OOP forces on you makes testing like this possible.


“Learn Object-Oriented Java the Hard Way” is ©2015–2016 Graham Mitchell