Quantcast

P^i

Your Online Tech Magazine

Fri05242013

Last update12:17:26 PM

Back You are here: Home More Programming and Web Programming Chapter 2 : Encapsulation

Chapter 2 : Encapsulation



Java - EncapsulationEncapsulation

Imagine you wrote the code for a class, and another dozen programmers from your company all wrote programs that used your class. Now imagine that later on, you didn't like the way the class behaved, because some of its instance variables were being set (by the other programmers from within their code) to values you hadn't anticipated. Their code brought out errors in your code. (Relax, this is just hypothetical.) Well, it is a Java program, so you should be able just to ship out a newer version of the class, which they could replace in their programs without changing any of their own code.

This scenario highlights two of the promises/benefits of Object Orientation: flexibility and maintainability. But those benefits don't come automatically. You have to do something. You have to write your classes and code in a way that supports flexibility and maintainability. So what if Java supports OO? It can't design your code for you. For example, imagine if you made your class with public instance variables, and those other programmers were setting the instance variables directly, as the following code demonstrates:

public class BadOO {

public int size;

public int weight;

...

}

public class ExploitBadOO {

public static void main (String [] args) {

BadOO b = new BadOO();

 

b.size = -5; // Legal but bad!!

}

}

And now you're in trouble. How are you going to change the class in a way that lets you handle the issues that come up when somebody changes the size variable to a value that causes problems? Your only choice is to go back in and write method code for adjusting size (a setSize(int a) method, for example), and then protect the size variable with, say, a private access modifier. But as soon as you make that change to your code, you break everyone else's! The ability to make changes in your implementation code without breaking the code of others who use your code is a key benefit of encapsulation. You want to hide implementation details behind a public programming interface. By interface, we mean the set of accessible methods your code makes available for other code to call—in other words, your code's API. By hiding implementation details, you can rework your method code (perhaps also altering the way variables are used by your class) without forcing a change in the code that calls your changed method.

If you want maintainability, flexibility, and extensibility (and of course, you do), your design must include encapsulation. How do you do that?

  • Keep instance variables protected (with an access modifier, often private).
  • Make public accessor methods, and force calling code to use those methods rather than directly accessing the instance variable.
  • For the methods, use the JavaBeans naming convention of set<someProperty> and get<someProperty>.

Figure 2-1 illustrates the idea that encapsulation forces callers of our code to go through methods rather than accessing variables directly.

The Nature of Encapsulation

We call the access methods getters and setters although some prefer the fancier terms accessors and mutators. (Personally, we don't like the word "mutate".)

Regardless of what you call them, they're methods that other programmers must go through in order to access your instance variables. They look simple, and you've probably been using them forever:

public class Box {

// protect the instance variable; only an instance

// of Box can access it " d " "dfdf"

private int size;

// Provide public getters and setters

public int getSize() {

return size;

}

public void setSize(int newSize) {

size = newSize;

}

}

Wait a minute...how useful is the previous code? It doesn't even do any validation or processing. What benefit can there be from having getters and setters that add no additional functionality? The point is, you can change your mind later, and add more code to your methods without breaking your API. Even if today you don't think you really need validation or processing of the data, good OO design dictates that you plan for the future. To be safe, force calling code to go through your methods rather than going directly to instance variables. Always. Then you're free to rework your method implementations later, without risking the wrath of those dozen programmers who know where you live.


Inheritance, Is-A, Has-A

Inheritance is everywhere in Java. It's safe to say that it's almost (almost?) impossible to write even the tiniest Java program without using inheritance. In order to explore this topic we're going to use the instanceof operator. For now, just remember that instanceof returns true if the reference variable being tested is of the type being compared to. This code:

class Test {

public static void main(String [] args) {

Test t1 = new Test();

Test t2 = new Test();

if (!t1.equals(t2))

System.out.println("they're not equal");

if (t1 instanceof Object)

System.out.println("t1's an Object");

}

}

Produces the output:

they're not equal

t1's an Object

Where did that equals method come from? The reference variable t1 is of type Test, and there's no equals method in the Test class. Or is there? The second if test asks whether t1 is an instance of class Object, and because it is (more on that soon), the if test succeeds.

Hold on…how can t1 be an instance of type Object, we just said it was of type Test? I'm sure you're way ahead of us here, but it turns out that every class in Java is a subclass of class Object, (except of course class Object itself). In other words, every class you'll ever use or ever write will inherit from class Object. You'll always have an equals method, a clone method, notify, wait, and others, available to use. Whenever you create a class, you automatically inherit all of class Object's methods. Why? Let's look at that equals method for instance. Java's creators correctly assumed that it would be very common for Java programmers to want to compare instances of their classes to check for equality. If class Object didn't have an equals method, you'd have to write one yourself; you and every other Java programmer.

That one equals method has been inherited billions of times. (To be fair, equals has also been overridden billions of times, but we're getting ahead of ourselves.)

For now you'll need to know that you can create inheritance relationships in Java by extending a class. It's also important to understand that the two most common reasons to use inheritance are

  • To promote code reuse
  • To use polymorphism

Let's start with reuse. A common design approach is to create a fairly generic version of a class with the intention of creating more specialized subclasses that inherit from it. For example:

class GameShape {

public void displayShape() {

System.out.println("displaying shape");

}

// more code

}

class PlayerPiece extends GameShape {

public void movePiece() {

System.out.println("moving game piece");

}

// more code

}

public class TestShapes {

public static void main (String[] args) {

PlayerPiece shape = new PlayerPiece();

shape.displayShape();

shape.movePiece();

}

}

Outputs:

displaying shape

moving game piece

Notice that the PlayingPiece class inherits the generic display() method from the less-specialized class GameShape, and also adds its own method, movePiece(). Code reuse through inheritance means that methods with generic functionality (like display())—that could apply to a wide range of different kinds of shapes in a game—don't have to be reimplemented. That means all specialized subclasses of GameShape are guaranteed to have the capabilities of the more generic superclass. You don't want to have to rewrite the display() code in each of your specialized components of an online game.

But you knew that. You've experienced the pain of duplicate code when you make a change in one place and have to track down all the other places where that same (or very similar) code exists. The second (and related) use of inheritance is to allow your classes to be accessed polymorphically—a capability provided by interfaces as well, but we'll get to that in a minute. Let's say that you have a GameLauncher class that wants to loop through a list of different kinds of GameShape objects, and invoke display() on each of them. At the time you write this class, you don't know every possible kind of GameShape subclass that anyone else will ever write. And you sure don't want to have to redo your code just because somebody decided to build a Dice shape six months later.

The beautiful thing about polymorphism ("many forms") is that you can treat any subclass of GameShape as a GameShape. In other words, you can write code in your GameLauncher class that says, "I don't care what kind of object you are as long as you inherit from (extend) GameShape. And as far as I'm concerned, if you extend GameShape then you've definitely got a display() method, so I know I can call it." Imagine we now have two specialized subclasses that extend the more generic GameShape class, PlayerPiece and TilePiece:

class GameShape {

public void displayShape() {

System.out.println("displaying shape");

}

// more code

}

class PlayerPiece extends GameShape {

public void movePiece() {

System.out.println("moving game piece");

}

// more code

}

class TilePiece extends GameShape {

public void getAdjacent() {

System.out.println("getting adjacent tiles");

}

// more code

}

Now imagine a test class has a method with a declared argument type of GameShape, that means it can take any kind of GameShape. In other words, any subclass of GameShape can be passed to a method with an argument of type GameShape. This code

public class TestShapes {

public static void main (String[] args) {

PlayerPiece player = new PlayerPiece();

TilePiece tile = new TilePiece();

doShapes(player);

doShapes(tile);

}

public static void doShapes(GameShape shape) {

shape.displayShape();

}

}

Outputs:

displaying shape

displaying shape

The key point is that the doShapes() method is declared with a GameShape argument but can be passed any subtype (in this example, a subclass) of GameShape. The method can then invoke any method of GameShape, without any concern for the actual runtime class type of the object passed to the method. There are implications, though. The doShapes() method knows only that the objects are a type of GameShape, since that's how the parameter is declared. And using a reference variable declared as type GameShape—regardless of whether the variable is a method parameter, local variable, or instance variable—means that only the methods of GameShape can be invoked on it. The methods you can call on a reference are totally dependent on the declared type of the variable, no matter what the actual object is, that the reference is referring to. That means you can't use a GameShape variable to call, say, the getAdjacent() method even if the object passed in is of type TilePiece. (We'll see this again when we look at interfaces.)








blog comments powered by Disqus