Learn Object-Oriented Java the Hard Way

Exercise 8: Failure to Encapsulate

In the previous exercise, we looked at extreme testing, and how encapsulation makes that possible. In this one, we’ll see some of the tradeoffs that can be made with fields and methods.

SphereCalc.java
 1 public class SphereCalc {
 2     double radius;
 3 
 4     public void setRadius( double r ) {
 5         radius = r;
 6     }
 7 
 8     public double getRadius() {
 9         return radius;
10     }
11 
12     public double getSurfaceArea() {
13         return 4*Math.PI*radius*radius;
14     }
15 
16     public double getVolume() {
17         return 4*Math.PI*Math.pow(radius,3) / 3.0;
18     }
19 }

This object is very similar to the ones in the last couple of exercises. A single instance variable this time, one mutator method (setRadius()) and three accessor methods. (The surface area of a sphere is  4 \pi r^2 , and the volume of a sphere is  \frac{4}{3} \pi r^3 .)

SphereCalcTester.java
 1 public class SphereCalcTester {
 2     public static void main( String[] args ) {
 3 
 4         SphereCalc c = new SphereCalc();
 5 
 6         c.setRadius(5);
 7         if ( isNear(c.getSurfaceArea(), 314.159265359) )
 8             System.out.println("PASS: surfaceArea for " + c.getRadius());
 9         else
10             System.out.println("FAIL: surfaceArea not what was expected!");
11         if ( isNear(c.getVolume(), 523.598775598) )
12             System.out.println("PASS: volume for " + c.getRadius());
13         else
14             System.out.println("FAIL: volume not what was expected!");
15 
16         c.setRadius(0.1);
17         if ( isNear(c.getSurfaceArea(), 0.125663706) )
18             System.out.println("PASS: surfaceArea for " + c.getRadius());
19         else
20             System.out.println("FAIL: surfaceArea not what was expected!");
21         if ( isNear(c.getVolume(), 4.18879E-3) )
22             System.out.println("PASS: volume for " + c.getRadius());
23         else
24             System.out.println("FAIL: volume not what was expected!");
25 
26 
27     }
28 
29     public static boolean isNear( double a, double b ) {
30         return Math.abs(a-b) < 1E-9;
31     }
32 }

What You Should See

This is clearly a tester and not just a simple driver program; I have tests that are passing or failing. Writing this was pretty annoying, but I wanted to show you the idea without making it too crazy, so I had to use my calculator with a couple of test cases to see what they ought to be. In a future exercise we’ll see a much better way to do a lot of tests like this without repeating so much code, but it’s too complicated for now.

Line 4 instantiates a SphereCalc object, then line 6 sets its radius to 5.

Starting down on line 29, there’s a little helper function I wrote. It receives two doubles and returns true if the absolute value of their difference is very small (smaller than 1.0 \times 10^{-9}). It’s best to avoid using just == on two floating-point values since sometimes repeating decimals or slight differences in rounding will make two values that ought to be the same slightly different.

(Instead of isNear() I probably could have called the function isVeryCloseToEqual() but I didn’t feel like typing that more than once.)

So lines 7 through 14 just call the methods from SphereCalc and make sure they return numbers close enough to the expected values. If so, we print out “PASS” and if not we print out “FAIL” and a little bit of detail. Normally you’d want to print out more information with the failure (like which radius failed and what the expected value was and what you got instead), but I didn’t want to clutter up the code.

Oh, and in case you’ve never seen it before, an E inside a floating-point number means “times ten to the”. On line 21, 4.18879E-3 means 4.18879 \times 10^{-3}) A.K.A. 0.00418879.

Okay, so now let’s look at an slightly different way of splitting up the work in the SphereCalc object. (You’ll need to type this one in, too, if you’re going to do the Study Drill.)

SphereCalc2.java
 1 public class SphereCalc2 {
 2     double radius, area, volume;
 3 
 4     public void setRadius( double r ) {
 5         radius = r;
 6         area = 4*Math.PI*r*r;
 7         volume = 4*Math.PI*Math.pow(r,3) / 3.0;
 8     }
 9 
10     public double getRadius() {
11         return radius;
12     }
13 
14     public double getSurfaceArea() {
15         return area;
16     }
17 
18     public double getVolume() {
19         return volume;
20     }
21 }

SphereCalc2 has three fields instead of just one. And inside the setRadius() mutator method, it doesn’t just set the radius, it also goes ahead and computes the surface area and volume, too.

There’s a trade-off here. Each instance of a SphereCalc2 object would take up slightly more memory than each SphereCalc object, because of the extra fields, and creating a instance of a SphereCalc2 object would take slightly longer than instantiating a SphereCalc object because it does more calculations up front.

However, if you had a SphereCalc object and you called getVolume() over and over again in a loop or something, it would have to do that calculation over and over. Whereas a SphereCalc2 object has already done the calculation and just gets to return that single value over and over.

Which approach is better? You’d have to run tests and see how your object is being used to find out.

SphereCalc2 has one serious problem, however. Well, it’s more like a vulnerability than a problem. When someone is using a SphereCalc2 object and they want to change the radius, we expect them to use the provided setRadius() method. We hope that’s what they will do.

But as you might recall from TVActorDriver.java way back in Exercise 4, a driver class can access instance variables directly. At least, the way we’ve been writing them up to this point.

What’s to prevent someone from writing code like this?

SphereCalc2 sph = new SphereCalc2();  
sph.setRadius(5);  
sph.radius = 7;   // OH NOES!  
System.out.println( sph.getVolume() );  

Now, it probably wouldn’t look so evil. It might be like on line 16 on the tester. Instead of writing:

c.setRadius(0.1);  // <-- why write this...  
c.radius = 0.1;    // <-- when it's SO much easier to write this?

It’s more efficient, right? Who wants to call a method when you can just put a value in a variable?!? Not this guy!

Anyway, hopefully that illustrates the “problem”. For the solution, you’ll have to come back in the next exercise.


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