Designing with Objects
By Al Williams
I've noticed as I teach classes that many programmers are rusty when it comes to doing traditional tasks. They often get wrapped up in user-interface jobs, like those that require event handlers and layout managers. Those jobs make it easy to skip core techniques, or forget them altogether.
More and more programmers are writing code for the server, where the user interface is practically nonexistent. If you find yourself focusing on algorithms instead of user interfaces, you might want to spend some time brushing up on object-oriented design skills. After all, most user-interface development consists of using Abstract Window Toolkit (AWT) or Swing classes, whereas server-side work often requires you to develop your own class hierarchies.
You can always put together a quick-and-dirty class definition, but a well-planned one makes your code more robust, easier to build, and simpler to maintain. In short, a properly designed program can make all the difference between success and virtual disaster. And don't be fooled thinking object-oriented design applies only to languages like Java or C++. As I'll show you later, you can create objects in JavaScript, too.
What's an Object?
The idea here is to get back to basics, so let's start right at the beginning. What's an object? In simple terms, an object is an entity that has "state," meaning that it can store information across sessions; the information isn't lost or reset each time your program code refers to the object.
An object can perform operations on itself, via methods or functions. For example, in programming languages that aren't object-oriented, there are a number of functions that you can use to open and modify a file. If you're reading from and writing to the file, you may have buffers that reside in memory and temporarily store the input and output characters. You might also have variables for keeping track of where you are in the file. If you were to use an object-oriented design, you could represent the file and all its related variables and functions as a single object. The object would make it easy to keep all the data (variables) together with all the possible operations (functions).
In traditional object-oriented parlance, you send messages to an object to tell it what to do. With Java, sending a message means calling a member function. You might call a function to tell the file to open itself, read data, or close itself. The biggest advantage is that you can change how the object works internally without affecting other programs. For example, if you were to rewrite your file object to make it more efficient, you wouldn't have to modify any other code in your program.
Your program creates objects based on a class that you define. Think of a class as a cookie cutter. You don't eat the cookie cutter, but it makes the cookies. A class is just a blueprint for an object. When you instantiate the object (for example, by using the new keyword in Java), you create a particular object that follows the plan laid out by the class.
The real trick to object-oriented design is deciding what constitutes an object. Do you encapsulate all functions and variables that relate to a file in one object, or do you select only specific items to be in the object? A good object is a focused representation of some entity in your design. Focus means not trying to do too much. For example, suppose you're developing a system that reserves conference rooms. Developing a single class to represent buildings, conference rooms, and meetings isn't an example of focus. Instead, you might decide to write one class to create building objects, one for conference room objects, and one for meeting objects.
Of course, too much focus can be burdensome, too. Creating objects to represent each individual chair in a meeting room is probably going too far.
Encapsulation
One of the key features of object-oriented programming is encapsulation. Encapsulation is the process of hiding as much internal detail of your object as possible, so that others can use your object without having to know how it works.
Java, along with other object-oriented languages, allows you to declare each method and variable either public or private, with the public and private keywords. Anything public can be used by the rest of your program to manipulate the object. Anything private is used by the object for its own internal workings. If you think of the conference room example, the light switch is public but the electrical wiring is private. How the light switch causes the room to be illuminated is not important. However, it's important that the public part of your objectthe light switch in this casealways behaves in the same way.
Suppose you develop the conference registration system so that it uses a flat file to store the information about the meetings. However, once your system becomes popular, you find out that the flat file database overloads quickly. With object-oriented design, solving this problem is fairly easy. You can rewrite the class that your database object is built on so that it uses an Oracle database instead of a flat file. As long as the new class still supports the original public methods and variables, you won't have to change anything else in the rest of your program. That's the power of encapsulation.
In a sense, an object acts like a black box to the rest of your program. Objects hide the internal mechanisms of related functions, and expose only the parts that the rest of your program needs. As long as you don't alter the public interface, you're free to change the private methods and variables without fear of breaking the entire system. You can add new things to the public interface, if necessary, but you shouldn't delete or change anything you've already made public.
A common problem (in fact, one that I often run into) is the temptation just to make everything public. This defeats the real purpose of encapsulationyou should resist this temptation. Hide private implementation details.
Polymorphism
One of the most important parts of an object-oriented system is polymorphism. This is just a fancy word for establishing "is a kind of" relationships between objects. For example, Windows and Linux are both kinds of operating systems. Windows 2000 is a kind of Windows. RedHat and Slackware are kinds of Linuxes (or should I say Linuxi?).
The idea behind polymorphism is to factor the common parts out of a series of objects. For example, operating systems, regardless of brand, often contain many of the same ideas and a lot of the same code. If you place all the code and data relating to operating systems in a single class, the classes that represent Windows and Linux can extend this base class. This allows the Windows and Linux classes to reuse the common code in the operating system class without having to duplicate efforts. It also allows your program to treat Windows or Linux objects as simple operating system objects. If your program deals mostly with generic operating system objects (as opposed to specific Windows and Linux objects), you won't have to make many changes to your code when it comes time to introduce the MacOS or PalmOS platforms into your program.
In my earlier example of a meeting room schedule, you probably won't find much use for polymorphism. However, if you extended your program to handle, say lunch rooms and bathrooms in addition to meeting rooms, you might find that you want a room class. The base room class might store information about doors and seats, but the extended lunch room class could store additional information regarding ovens and microwaves.
With Java you typically get polymorphism (and the associated code reuse) by extending one class from another with the extend keyword.
I should mention that you can also get polymorphism without code reuse by using an interface. An interface in Java acts like a skeleton class that defines a number of required functions but doesn't actually implement the inner workings of those functions.
When you define an interface, you don't write any codeyou simply provide a list of functions that the interface contains. Any class that implements the interface (with the implement keyword) must provide the actual code for the functions. All classes that implement a particular interface are polymorphic with each other. This is handy for cases where objects are similar to another object, but not enough so that you want to share code among them.
Class Relationships
Recent versions of Java offer several other ways to express class relationships. You can nest classes, make one class a member of another, create local classes, or create anonymous classes. None of these is strictly necessary to create an object-oriented program, but they do offer more options for grouping code together and can help in many common situations (like event handling, for example). I'll use several of these techniques later, when I present the code for a basic conference scheduling program.
Nested classes. Java allows you to nest class definitions, one inside the other. A nested class is a static member of its parent class (see
Example 1). The nested class behaves just like an ordinary class, except to access it, you'd use datanode.converter. This is strictly a notational convenience. The converter class is just like any other class, but it is, by name, inside the datanode class.
Member classes. If you omit the static modifier in a nested class, you create what's called a member class. Any time you create an object from the parent class, an object will be created from the member class. In theory, the functions of the member class can refer to all the fields of the containing class, including private fields. You'll find, though, that some versions of the JDK don't support access to private fields from a member class even though they are supposed to.
Local classes. To define a local class, you simply begin a class declaration inside some piece of code, just like you would a local variable (see
Example 2). Like a member class, a local class can access portions of the surrounding class. Unlike a member class (but like a local variable), a local class is only visible in the scope that defines it. In the example, the helper class is only visible to the code in somefunction, but not to the code in anotherfunction.
Anonymous classes. A close relative to the local class is the anonymous class. It's essentially a local class with no name. Since it has no name, you always instantiate the class right along with its definition (see
Example 3). If you've used Java's new event handling, you've probably used anonymous classes before.
The Conference Scheduler
How can you put classes and objects to work? Consider the original problem: scheduling meeting rooms. To simplify things, assume there's only a single building that contains all the rooms. The building can serve as a master object, more or less controlling the other objects. This leaves us with two other major entities the program must model.
First, there's the room. The room has a capacity, and it will also have to know when it's scheduled for use, so we'll write a class that can be used to create room objects. The other major class in this system creates meeting objects. A meeting object should be able to store the responsible party's name, the start and end time, and the number of attendees. You could get carried away and decide to make a class to represent a span of time; however, I decided to draw the line and use simple dates for the start and the end.
Since I have future plans for this system, I decided to make meetings a type of event. I created a base class that can be extended by specific event classes, and named it EventObj. (Don't confuse this with Java events, which deserve a whole article to themselves.) For the time being, I won't really make use of this distinction, but in the future, I might have other events like holidays, lectures, or deadlines.
Listing 1,
Listing 2, and
Listing 3 show the class definitions for the system.
Listing 4 contains a sample main program to test the system.
You'll notice that I exposed many variables (like the capacity of each room) as public. Many purists would argue that you should keep all variables private and write functions (like getCapacity and setCapacity) to manage the variables. That's often good advice, and almost a necessity when working with JavaBeans, due to the conventions for Beans. Using public functions to access a private variable increases the object's encapsulation. You can change how the object stores the variable without upsetting the public interface. However, in this case there is no value to adding the extra overhead, so I didn't do it.
Rooms only occur as part of a building. That's why I made the Room class a nested class of Building. Each object knows how to perform operations on itself. For example, the Room object can test a time span for conflicts.
The Building object can provide an Enumeration object, which essentially serves as a list of rooms. This is a good example of a case where you would want to implement an interface instead of extending an object. There's no standard code available for a generic enumeration (because the code depends on the data type involved), so code reuse isn't an issue. Because the object in question is an anonymous class, there are a few things different about this class definition, like the odd syntax:
new Enumeration { ... }
Normally, you'd think you can't instantiate Enumeration because it is an interface. However, in this case Enumeration really is the base for the anonymous class that follows. Because the class has no name, it can't have a constructor. Luckily, you can use another new feature of Java to compensatean instance initializer. This is nothing more than a block of code (within braces) that appears in the class definition. Just as fields are initialized when your program first creates an object, the instance initializer code executes too. In practice, this is as good as a constructor that doesn't take any arguments.
Object-Oriented Opportunity
When designing objects, remember to keep each object focused, make each object as self-contained as possible, and factor common code into base classes.
Having a hammer doesn't mean you can build a house. Using an object-oriented tool like Java doesn't mean you're writing object-oriented code. Look for ways to use objects in your coding, even in JavaScript.
With so many ways to model objects, you're sure to come up with an elegant, succinct representation for nearly any problem. Elegant representations tend to generate elegant implementations. Better still, a great implementation will be more robust and maintainable than an ad hoc solution.
(Get the source code for this article here.)
Al is the author of many popular programming guides, including books on Active X, Active Server Pages, and C++ programming. You can find him on the Web at www.al-williams.com.