Exploiting Java's Object-Oriented Features

  From the Pages K.V. Viswanathan is a Curriculum Development Manager at ObjectShare, Inc., Santa Ana, CA. He can be contacted at [email protected].

THE MAIN PILLARS on which Java stands are platform independence and object orientation. While the developer community has been very quick to exploit the former, it has either been largely blind to the latter or is insufficiently tutored in the principles of object orientation to exploit it effectively. Java is gathering momentum, and unless there is widespread commitment to utilizing the object-oriented features of the language, the world will rapidly accumulate a mass of poorly written Java code. I provide here some pointers to developers through an example. My chief aim is to motivate Java programmers to exploit the object-oriented features of the language.

The Java Programming language has, in a very short time, evolved from being a "nice idea" to one that nearly every organization is very seriously evaluating as a possible universal platform of the future. The language itself has been evolving rapidly, with significant enhancements appearing at regular intervals. Issues identified as bottlenecks to widespread adoption are being addressed very seriously. Java is most definitely here to stay, and, over the next few years, it is very likely that more Java code will be written than code in any other programming language.

The developer community that is migrating to Java is doing so from a variety of backgrounds. A significant percentage is likely to be doing so from a non-object-oriented background. A steep learning curve is associated with object-orientation. A look at the current Java tutorial books available, quickly reveals that the focus is on language syntax and on using its class library. Almost no attention is being paid to teaching good object-oriented programming principles. Development organizations are jumping into Java projects without preparing the ground to reap the benefits of object orientation. Just adopting Java as the platform is certainly not guaranteed to bring in the benefits of object orientation. Several authors have pointed out that O-O is not a "silver bullet." Every Java developer needs to be committed to exploiting the full power of the O-O features in Java. Combined with its extensive and growing class library, and the rapid enhancements being made in dynamic compilation, it will become a force to reckon with in the very near future. It will be in the best interest of the IS community and every Java developer to prepare for the coming revolution by appreciating the key issues involved.

My aim is to provide some of the conceptual inputs, and, through an example, excite Java developers into making a serious commitment to "O-O development in Java" rather than just "Java development." We'll take a very simple application and delve into how to view even this simple application from an object-oriented perspective. The problem we consider is intentionally very simple because we do not want to cloud the issues being discussed by introducing other sources of complexity. The lessons to be learned are universal and can be applied to all problems, large or small.

Consider the problem of taking a number and determining whether it is a prime number or not (a number is said to be prime if it is divisible only by itself and 1). Object orientation is about programming in the large, and thus we will not focus on the actual algorithm for determining primality—that is the subject matter of algorithm design. We will simply use an existing approach. The reader could even choose to ignore that aspect of this article entirely.

Listing 1 shows a "correct" solution. It is quite representative of average Java code that one finds today. But is it object-oriented? Even though the code is in a class and the processing is broken up into two methods, this code has very little binary reusability. Of course, if the static readInt() method had been public, it could be reused. Reuse of static methods is not useless, but it is often nothing more than the "function-library" type of reuse from the procedural programming era. The full benefits of object-orientation are not to be found in this manner. The code for checking primality could also have been moved into another static method of its own, and would represent an improvement in quality (via functional decomposition), but that is not going to make the code any more object-oriented.

Good object-oriented design would take even a simple problem such as this and carve out reusable pieces from it. Of course, there is an infinite number of ways in which one could carve up a piece of code into multiple classes. The breaking-up process is guided by the needs of the situation. In object-orientation, the key goal is reuse, and one thinks in terms of assembling applications out of components. In the present situation, what components could we wire-up to build our application?

One approach is to think of our application as consisting of a user interface distinct from the main functionality, namely, that of checking for primality. Thus, we would separate those two aspects into distinct classes. Prying apart the two pieces in this manner provides the following immediate benefits:
  • The prime checker can now work with different user interfaces (e.g., text and graphical).

  • The user interface could be used in its own right in other problems (any problem that takes a number as input and does something based on it).
Two reusable components would be created where there were none before.
This approach looks promising and we will take it further. In fact, it turns out to be an application of a common idea in object-orientation—the "Model-View-Controller" (MVC) paradigm, which suggests that we should separate the user interface (view) of any application from its core (model). It also suggests that the model and view should not be tightly coupled to each other, so that they can plug-and-play in different situations on their own. Hence, the controller that mediates between them. The code we will now develop illustrates these ideas.

Thus, our design is going to involve three classes:
  • model class—which will contain a number and report on whether it is prime or not.
  • view class—which will do the work of presenting the user interface.
  • controller class—which will tie the model and view together.
The model class is straightforward. It keeps a single number as an instance variable, and whenever requested, tests it for primality. The model class is kept completely free of any issues other than its main job.

We would like to maximize reusability of code that we develop. There are two sides to the reuse coin—component and framework. This is best brought out by an analogy with electronic hardware, where integrated circuits (ICs) are plugged into circuit boards. ICs are analogous to components, and circuit boards are analogous to frameworks. Unless both are designed in a compatible manner, we will not gain reusability of either. There is not much use in having a great IC unless there is a suitable circuit board to plug it into. Equally useless is the most advanced circuit board unless there are components out there that will fit into it. The same applies to object-oriented design.

When developing our model class, we would like to think of it as a component that can be plugged into a framework. This will allow the framework to work not only with the present model class that we are developing but also with any new ones that we may develop in the future. Also, our model class will be able to plug into any other compatible framework. Thus, rather than developing the model class in isolation, let us create a Java interface NumberProblemModel and make our model class implement that interface. We then develop other classes solely in terms of the NumberProblemModel interface and avoid tightly coupling them with any specific kind of number problem. In doing this, we are following a general guideline that recommends building applications around interfaces rather than tying them to specific classes.1 The NumberProblemModel interface is shown in Listing 2.

We can imagine numerous classes that might implement the same interface and do other useful things. With this interface in place, we are now ready to implement the model class that checks for primality. We have designed the solve() method in the interface to return an Object because different types of problems may return different types of results (in a more extensive implementation one might define a separate class to describe the type of the return value from the solve() method). The getProblemName() method makes our model self-describing. The code of the model class, MyPrimeTester is given in Listing 3 (Listings 3-9 available only at Java Report Online).

Developing the view is a bit more complex because we want to be able to use our model in a text or graphical user interface (GUI). What does a view need to be able to do? If we go simply by the code in MyFirstPrimeTester, then we might decide that the user interface is controlled by the application; that is, we might design the view class in such a way that it provides a number in response to an explicit request like getNumber( ). This will pose a problem if we want our model to be able to work with both text and GUIs. GUIs allow the user to control application flow. In these situations, the application presents the user interface (UI) and lets the UI take control of the flow. The UI then communicates with the rest of the application through events. We will therefore design even our text UI to act according to the event-based approach.

Figure 1
Figure 1. The flow of events using a UML sequence diagram.

In this scheme of things, the controller first registers interest with the view and waits to be notified about events occurring at the UI. When an event occurs, the view lets the controller know about it by invoking a suitable method on it. Figure 1 shows the flow of events using a UML sequence diagram (without details of method arguments). In Figure 1, after the controller has registered interest with the view, the user enters a number and the view then lets the controller know about it by invoking the numberEntered( ) method. In response, the controller sets the model in motion by first setting the number for the problem to be solved by invoking the setNumber() method, and then invoking the solve() method on the model. The controller then asks the view to display the result. We will delve into these details as we develop the other classes.

Notice that there is no direct communication between the view and the model. The controller mediates their interactions. To enable these communications to take place, the view needs a reference to the controller, and the controller needs a reference to the model. How do we design these associations?

We will implement both of these associations through instance variables. What should be the type of the instance variable in the view? Can it be an instance of the model class? Surely not, because the view only communicates with the controller. If we make the view get involved directly with the model, then it is not going to be generic enough to be reused in other situations.

Does the view really care about the class of the objects that are interested in its services? No; instances of any class can use a given view. In fact, it is important not to tightly couple the view to work with only instances of a specific class. De-coupling of this form is precisely what makes it possible to develop plug-and-play systems.

It is thus clear that the instance variable in view that will hold the reference to the controller should ideally not be of any class type. We will make it an interface type called NumberListener. Any class that wants to use the view would implement NumberListener. This is how we allow the same view to plug-and-play with many classes.

The code for NumberListener is given in Listing 4. (In practice, we would want to conform to the JavaBeans specification, have our interface extend java.util.EventListener, and also have a special event class that extends java.util.EventObject defined for this application. In the interest of brevity, we are avoiding those details here.)

Our view class will have an instance variable of type NumberListener. When an event takes place (user enters a number, user wants to quit the application) the view will notify the listener by invoking the appropriate method on that listener.

The view and controller are interdependent; they need to talk to each other. We have already seen that the view needs to notify the controller of events. However, the controller needs to also know about the view; it has to register interest in the first place so that it can be notified when events take place. So we go through the same set of arguments as first discussed. What type of object does the controller think it is talking with? It does not really care who provides it with the data. Once again we develop an interface, NumberProblemView, which lays out the basic message protocol of the UI. The code for NumberProblemView is shown in Listing 5.

Based on the two interfaces NumberListener and NumberProblemView, we are ready to build the text UI class. Listing 6 shows the code.

Now that we have the model and view in place, we are in a position to design the controller class, which will mediate between the model and the view. Once again, we will try to make the controller generic. The code in the controller is independent of any specific model or view class.

The controller has a constructor that allows us to specify both the UI to be used as well as the problem to be solved. The types of both these arguments have been specified as the corresponding interface types (NumberProblemView and NumberProblemModel) and not the actual classes. This way the same controller can be used with any new classes we choose to design. For example, we might design a GUI for number problems, and we might develop other model classes that implement the NumberProblemModel interface. The same controller will work with all of them, unchanged. It also has a main method that can be used to start up the application. In anticipation of new view and model classes that may be developed, we have designed the static main() method in controller to accept two command line arguments that supply the class names of the view and model classes, respectively. The main() method gets the class descriptors of these classes by using the Class.forName() method and then creates instances by invoking the newInstance() method on the resulting class descriptors. The results from the two newInstance() invocations are suitably typecast. The code for NumberProblemController is given in Listing 7.

Note how the code in all the classes has been made very generic. They are all coded terms of interfaces and can thus be reused in binary form with new classes that we might develop in the future.

We now present a GUI class NumberProblemGUI in Listing 8, and another model class MyFactorAdder in Listing 9 (which computes the sum of all the divisors of a given number). Both of these plug-and-play with the existing classes, thereby demonstrating the validity of this approach to building reusable classes.

To really benefit from using Java as a language to build systems, we need to fully exploit its object-oriented features. This is an aspect that is today receiving less attention than it ought to. I've shown, by example, how to develop applications around interfaces, rather than make them dependent on specific classes and mar reusability.

Inheritance is also commonly used as a mechanism to make multiple classes share a common external interface. However, inheritance is basically a code-reuse mechanism. It should be used only when subclasses can inherit significant amounts of code. Premature decisions on inheritance can cause problems later on. However, if one chooses the interface route early on, it would still be possible to opportunistically adopt inheritance wherever it proves to be appropriate. An interface-based design enhances the extensibility of a system by keeping open the possibility that future classes from an entirely unrelated branch of the class hierarchy will be able to plug-and-play with the classes we have developed.

We have utilized an application of the MVC paradigm as our example. The MVC paradigm is widely used within Smalltalk environments to decouple models and UIs. Java has also adopted the MVC approach in its new swing components. What we showed here is a simplified version of MVC. In a typical implementation, the model would be capable of keeping track of multiple listeners and would notify them whenever it changed. In our implementation the model was incapable of changing on its own. It was spurred into action only by events happening at the UI and therefore did not need to keep track of any listeners.

The benefits of MVC come at the price of increased complexity. Instead of the one class in our initial design, we ended up with six (including the interfaces). This makes the application more difficult to comprehend. There could also be efficiency implications because of the increased number of objects involved; but that is hardly an issue in the UI portion of an application, where the user's reaction time will easily offset any such inefficiency. Further, the execution overhead imposed by MVC is very small. The increased reusability and maintainability easily justify the added complexity. Environments like VisualBasic and PowerBuilder tend to combine the view and the controller into one entity, thereby compromising the reusability of the view. When the view and controller are combined, the controller portion of code will be repeated when the same model needs a different view. In general, MVC has been widely accepted. The only conceivable situations when one would not pay the price of the increased complexity would be in trivial, one-off applications, and in applications where there is absolutely no possibility of a model requiring more than one view.

1. Gamma, E. et al., Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, 1995.

Quantity reprints of this article can be purchased by phone: 717.560.2001, ext.39 or by email: [email protected].