This series, The Object-Oriented Thought Process, is intended for someone just learning an object-oriented language and wants to understand the basic concepts before jumping into the code or someone who wants to understand the infrastructure behind an OOP language they are already using. Click here to start at the beginning of the series.
In keeping with the code examples used in the previous articles, Java will be the language used to implement the concepts in code. One of the reasons that I like to use Java is because you can download the Java compiler for personal use at the Sun Microsystems Web site http://java.sun.com/. You can download the J2SE 1.4.2 SDK (software development kit) to compile and execute these applications—I will provide the code listings for all examples in this article. I have the SDK 1.4.0 loaded on my machine. I will also provide figures and the output (when appropriate) for these examples. See the previous articles in this series for detailed descriptions for compiling and running all the code examples. Click here to start at the beginning of the series.
In the last column, you took a look inside the bytecodes produced by constructors. Using the javap tool, provided by the SDK, you were able to see that, even if the programmer does not include a constructor in the code, the compiler will provide a default constructor automatically.
Constructors provide a good mechanism to support encapsulation. By designing proper constructors, you can properly initialize your encapsulated data. There is really no point to encapsulating your data if you fail to place your system in a safe state. Encapsulation is perhaps the most important rule that an object-oriented designer/programmer must adhere to. However, is Encapsulation as sacred as many software development pundits profess? In this column, you will explore this question, which turns out to be quite interesting.
Inheritance Revisited
As mentioned earlier in this series, one of the most powerful attributes of O-O programming is code reuse. Procedural programming provides code reuse to a certain degree—you can write a procedure and then use it as many times as you want. However, O-O programming goes an important step further, allowing you to define relationships between classes that facilitate not only code reuse, but also better overall design, by organizing classes and factoring in commonalties of various classes. Inheritance is the primary means of providing this functionality.
Inheritance allows a class to inherit the attributes and methods of another class. This allows you to create brand new classes by abstracting out common attributes and behaviors.
One of the major design issues in O-O programming is to factor out commonality of the various classes. For example, say you have a Dog class and a Cat class, and each will have an attribute for eye color. In a procedural model, the code for Dog and Cat would each contain this attribute. In an O-O design, the color attribute can be abstracted up to a class called Mammal—along with any other common attributes and methods. In this case, both Dog and Cat inherit from the Mammal class, as shown in Figure 1.
Figure 1: Mammal hierarchy.
The Dog and Cat classes both inherit from Mammal. This means that a Dog class actually has the following attributes:
eyeColor // inherited from Mammal
barkFrequency // defined only for Dogs
In the same vein, Dog object has the following methods:
GetEyeColor // inherited from Mammal
Bark // defined only for Dogs
When the Dog or the Cat object is instantiated, it contains everything in its class, as well as everything from the parent class. Thus, Dog has all the properties of its class definition, as well as the properties inherited from the Mammal class.
Encapsulation Revisited
One of the primary advantages of using objects is that the object need not reveal all of its attributes and behaviors. In good O-O design (at least what is generally accepted as good), an object should only reveal the interfaces needed to interact with it. Details not pertinent to the use of the object should be hidden from other objects. This is called encapsulation.
As discussed earlier in this series, the interface is the fundamental means of communication between objects. Each class design specifies the interfaces for the proper instantiation and operation of objects. Any behavior that the object provides must be invoked by a message sent using one of the provided interfaces. The interface should completely describe how users of the class interact with the class. In Java, the methods that are part of the interface are designated as public; everything else is part of the private implementation.
The Encapsulation Rule
Whenever the interface/implementation paradigm is covered, you are really talking about encapsulation. The basic question is what in a class should be exposed and what should not be exposed. This encapsulation pertains equally to data and behavior. When talking about a class, the primary design decision revolves around encapsulating both the data and the behavior into a well-written class.
Stephen Gilbert and Bill McCarty define encapsulation as “the process of packaging your program, dividing each of its classes into two distinct parts: the interface and the implementation.” This is the message that has been presented over and over again in this series.
But what does encapsulation have to do with inheritance, and how does it apply with regard to this series? Encapsulation is so crucial to O-O development that it is one of the generally accepted object-oriented design’s cardinal rules. Yet, Inheritance is also considered one of the three primary O-O concepts. However, in one way, inheritance actually breaks encapsulation! How can this be? Is it possible that two of the three primary concepts of O-O are incompatible with each other? Well, let’s explore this possibility.
Recall that there are three criteria that determine whether or not a language is object-oriented: encapsulation, inheritance, and polymorphism. To be considered a true object-oriented language, the language must support all three of these concepts.
How Inheritance Weakens Encapsulation
As already stated, encapsulation is the process of packaging classes into the public interface and the private implementation. In essence, a class hides everything that is not necessary for other classes to know about.
Peter Coad and Mark Mayfield make a case that, when using inheritance, encapsulation is inherently weakened within a class hierarchy. They discuss a specific risk: Inheritance indicates strong encapsulation with other classes, but weak encapsulation between a superclass and its subclasses.
The problem is that if you inherit an implementation from a superclass and then change that implementation, the change ripples through the class hierarchy. This rippling effect potentially affects all the subclasses. At first, this might not seem like a major problem; however, as you have seen, a rippling effect such as this can cause unanticipated problems. For example, testing can become a nightmare. In earlier columns, you covered how encapsulation makes testing systems significantly easier. In theory, if you create a class called Cabbie (see Figure 2) with the appropriate public interfaces, any change to the implementation of Cabbie should be transparent to all other classes. If the other classes were directly dependent on the implementation of the Cabbie class, testing would become more difficult, if not untenable.
Note: Even with encapsulation, you would still want to retest the classes that use Cabbie to verify that no problem has been introduced by the change.
Figure 2: A UML diagram of the Cabbie class.
If you then create a subclass of Cabbie called PartTimeCabbie, and PartTimeCabbie inherits the implementation from Cabbie, changing the implementation of Cabbie directly affects the PartTimeCabbie class.
For example, consider the UML diagram in Figure 3. PartTimeCabbie is a subclass of Cabbie. Thus, PartTimeCabbie inherits the public implementation of Cabbie, including the method giveDirections(). If the method giveDirections() is changed in Cabbie, it will have a direct impact on PartTimeCabbie and any other classes that might later be subclasses of Cabbie. In this subtle way, changes to the implementation of Cabbie are not necessarily encapsulated within the Cabbie class.
Figure 3: A UML diagram of the Cabbie/PartTimeCabbie classes.
To reduce the risk posed by this dilemma, it is important that you stick to the strict is-a condition when using inheritance. If the subclass were truly a specialization of the superclass, changes to the parent would likely affect the child in ways that are natural and expected. To illustrate, if a Circle class inherits implementation from a Shape class, and a change to the implementation of Shape breaks Circle, then Circle was not truly a Shape to begin with.
How can inheritance be used improperly? Consider a situation in which you want to create a window for the purposes of a graphical user interface (GUI). One impulse might be to create a window by making it a subclass of a rectangle class:
class Rectangle {
}
class Window extends Rectangle {
}
Listing 1
In reality, a GUI window is much, much more than a rectangle. It is not really a specialized version of a rectangle, as is a square. A true window might contain a rectangle (in fact, many rectangles); however, it is really not a true rectangle. In this approach, a Window class should not inherit from Rectangle, but it should contain Rectangle classes.
class Window {
Rectangle menubar;
Rectangle statusbar;
Rectangle mainview;
}
Listing 2
A Code Example
Take a look at a specific code example that illustrates the problem that exists between encapsulation and inheritance. Consider the following class called Mammal presented in Listing 3.
// Class Mammal public class Mammal { private String color; public void growHair(){ System.out.println("Hair Growing"); } }
Listing 3
In this class, you have a private attribute called color and a public method called growHair(). To use the Mammal class, you create an application called Packaging (the choice of this name will become clear later). This class is presented in Listing 4.
//Class Packaging public class Packaging { public static void main (String args[]){ Mammal mammal = new Mammal(); mammal.growHair(); mammal.color = "blue"; } }
Listing 4
When you run this application, you get the output in Figure 4.
Figure 4
You can now test your encapsulation principle by attempting to have the application access the private attribute color as seen in Listing 5.
//Class Packaging public class Packaging { public static void main (String args[]){ Mammal mammal = new Mammal(); mammal.growHair(); mammal.color = "blue"; // try to access private attribute } }
Listing 5
As you can see in Figure 5, the compiler just won’t accept this.
Figure 5
This was all expected; however, try to add inheritance to the mix by adding a class called Dog, as seen in Listing 6.
//Class Dog public class Dog extends Mammal{ private int barkFrequency; public void bark(){ System.out.println("Dog Barking"); } }
Listing 6
Now, you can start doing some interesting things. First, you can try to access Mammal’s attribute color from the Dog class—see Listing 7.
//Class Dog public class Dog extends Mammal{ private int barkFrequency; public void bark(){ System.out.println("Dog Barking"); color = "blue"; } }
Listing 7
When you run this, the compiler complains again as seen in Figure 6. Yet, this may seem incorrect. Because Dog Is-A Mammal, doesn’t the Dog contain all the attributes of Mammal? In short, is it not true that Dog inherits all the attributes and methods of Mammal?
Figure 6
This is an interesting design dilemma for object-oriented compiler builders. Here is the problem.
- Inheritance dictates that a child class (subclass) inherits all the attributes and methods from a parent class (superclass).
- Encapsulation dictates that one class must not have access to the (private) data of another class.
The way these rules are written, it seems that they are mutually exclusive. How can this conundrum be addressed?
Because encapsulation is the primary object-oriented mandate, you must make all attributes private. Making the attributes public is not an option. Thus, it appears that the use of inheritance may be severely limited. If a subclass does not have access to the attributes of its parent, this situation presents a sticky design problem.
To allow subclasses to access the attributes of the parent, language designers have included the access modifier protected. There are actually two types of protected access, which I will explain in detail later. Right now, change the color access modifier to protected, as seen in Listing 8.
// Class Mammal
public class Mammal {
protected String color;
public void growHair(){
System.out.println(“Hair Growing”);
}
}
Listing 8
This change allows the code to compile and execute. By making color protected, Dog now has access to the attributes in Mammal—as you would expect. However, you now have another problem.
In listing 9, I put back the line to access Dog’s color attribute from the application.
//Class Packaging
public class Packaging {
public static void main (String args[]){
Mammal mammal = new Mammal();
mammal.growHair();
mammal.color = “blue”;
}
}
Listing 9
The problem is that this now works (see Figure 7). In short, the application (class Packaging) can directly access Dog’s color attribute. Based on the rule of encapsulation, this is unacceptable. It appears that you have fixed the inheritance issue, but now you have broken encapsulation. What can you do?
Figure 7
You clearly have a dilemma. It appears that you have to make a decision as to whether to enforce inheritance or encapsulation. This example clearly shows the problem that inheritance poses to encapsulation. It is very helpful for a developer to fully understand the subtleties involved with the inner workings of various object-oriented constructs. By realizing what the issues are in this example, you can more fully grasp what inheritance and encapsulation are and how to make your designs more robust.
Conclusion
Many of the decisions that you need to make will be dictated to you by the implementation of the language that you choose. In Java, you can use the protected access modifier. In fact, there are two types of protected access modifiers that you need to understand before you can make the best possible design decision. You also need to take advantage of the Java packaging rules. This is why the application was named Packaging—in anticipation of next month’s column. As you may have already guessed, next month’s column will cover the use of the protected access modifiers along with the use of packages.
References
Gilbert, Stephen, and Bill McCarty: Object-Oriented Design in Java. The Waite Group, 1998.
Tyma, Paul, Gabriel Torok and Troy Downing: Java Primer Plus. The Waite Group, 1996.
Coad, Peter, and Mark Mayfield. Java Design. Object International, 1999.
Jaworski, Jamie: Java 1.1 Developers Guide. Sams Publishing, 1997.
About the Author
Matt Weisfeld is a faculty member at Cuyahoga Community College (Tri-C) in Cleveland, Ohio. Matt is a member of the Information Technology department, teaching programming languages such as C++, Java, and C# .NET as well as various web technologies. Prior to joining Tri-C, Matt spent 20 years in the information technology industry gaining experience in software development, project management, business development, corporate training, and part-time teaching. Matt holds an MS in computer science and an MBA in project management. Besides the first edition of The Object-Oriented Thought Process, Matt has published two other computer books, and more than a dozen articles in magazines and journals such as Dr. Dobb’s Journal, The C/C++ Users Journal, Software Development Magazine, Java Report, and the international journal Project Management. Matt has presented at conferences throughout the United States and Canada.