This month, I had intended to leap directly into the implementation of an object-oriented user-interface (OOUI) implementation framework. Looking over what I was writing, however, I came to the realization that there was no point in talking about OOUI architecture and implementation without first explaining why such applications have particular needs and must be implemented in certain ways. In other words, it seemed like a good idea to start out with the big picture before zooming into the details.
"Build user interfaces for object-oriented systems": Read the whole series!
- Part 1. What is an object? The theory behind building object-oriented user interfaces
- Part 2. The visual-proxy architecture
- Part 3. The incredible transmogrifying widget
- Part 4. Menu negotiation
- Part 5. Useful stuff
- Part 6. An RPN-calculator application demonstrates object-oriented UI principles
I've actually written about this sort of stuff before, but not for JavaWorld, and the articles I've written now are out of date, so I thought I'd take another stab at the problem of describing object-oriented systems from the UI designer's point of view. Hopefully, by the time I'm done you'll understand in general terms how a UI in an object-oriented system has to work. I'll also endeavor to clarify why not one of the RAD tools on the market today (Visual Café, Visual Age for Java, Visual J++, JBuilder -- you name it) is usable if your final goal is a well-crafted object-oriented system.
This month's article, then, is a primer on objects. It describes what an object is (or what it should be in a well-constructed system) and how objects should interact with one another at runtime. We'll also see how object-oriented systems, by nature, have UI-design requirements that simply don't come up in procedural systems, so are rarely implemented correctly by procedural programmers who are moving into an object-oriented environment. In subsequent articles, I intend to apply these principles to real code so that you can see how it all works. Next month, for example, I'll start looking at a forms-based I/O system that works well in an object-oriented environment. This article is a bit on the long side, but the concepts are essential, and I want to be as thorough as possible in my coverage.
Everything you know is wrong! Key concepts of object-oriented design
Bjarne Stroustrup, the inventor of C++, once characterized object-oriented programming as "buzzword-oriented programming," and certainly one of the most abused (or at least misunderstood) buzzwords in the pack is object itself. Since the idea of the object is so central, a full discussion of what objects actually are is essential to understanding object-oriented systems and their needs, particularly from the perspective of the user interface.
First of all, think of an object-oriented system as a bunch of intelligent animals (the objects) inside your machine talking to each other by sending messages back and forth. Think object. Classes are irrelevant -- they're just a convenience provided for the compiler. The animals/objects that comprise our system can be classified together if they have similar characteristics (if they can handle the same messages as other objects in the class, for example), but what you have at runtime is a bunch of objects, not classes -- animals, not their descriptions. What we programmers call classes are really classes of objects. That is, a set of objects are of the same class if they have the same properties. This usage is just English, and is really the correct way to think about things. We're doing object-oriented design, not class-based design.
The prime directive of object-oriented design is data abstraction. This is the CIA, need-to-know-basis school of program design. All information is hidden. A given object doesn't have any idea of what the innards of other objects look like, any more than you might know what your spouse's gall bladder looks like. (In the case of both the object and the gall bladder, you really don't want to know, either.)
So what, exactly, is an object?
You may have read in a book somewhere that an object is a datastructure of some sort combined with a set of functions, called methods, that manipulate that datastructure. Balderdash! Poppycock! First and foremost, an object is a collection of capabilities. An object is defined by what it can do, not by how it does it -- and the data is part of "how it does it." In practical terms, this means that an object is defined by the messages it can receive and send; the methods that handle these messages comprise the object's sole interface to the outer world. The emphasis must be on what an object can do -- what capabilities it has -- not on how those capabilities are implemented. The data is irrelevant. Most object-oriented designers will spend weeks, if not months, in design before they even think about the data component of an object. Of course, most objects will require some data in order to implement their capabilities, but the make-up of that data is -- or at least should be -- irrelevant.
I'll explain the whys and wherefors in a moment, but here are some rules of thumb that you can apply to see if you're really looking at an object-oriented system:
- All data is private. Period. (This rule applies to all implementation details, not just the data.)
setfunctions are evil. (They're just elaborate ways to make the data public.)
- Never ask an object for the information you need to do something; rather, ask the object that has the information to do the work for you.
- It must be possible to make any change to the way an object is implemented, no matter how significant that change may be, by modifying the single class that defines that object.
- All objects must provide their own UI.
If the system doesn't follow these rules, it isn't object-oriented. It's that simple. That's not to say non-object-oriented systems are bad; there are many perfectly good procedural systems in the world. Nonetheless, not exposing data is a fundamental principle of object-oriented systems. If you violate your principles, you're nothing, and the same goes for object-oriented systems. If a system violates object-oriented principles, it isn't object-oriented; it's some sort of weird hybrid that you may or may not ever get to work right.
Academic purity vs. in-the-trenches know-how
I should say that although the foregoing attitude might sound extreme, my ideas don't stem from some academic notion of purity; rather, every time I've strayed from the straight and narrow in my own work, I've found myself back in the code fixing it a month or two later. All of this do-it-wrong-then-go-back-and-fix-it work just started taking up too much time. It turned out to be easier to just do it right to begin with. My notions, then, are based on practical experience gathered while putting together working systems, and a desire to put those systems together as quickly as possible.
Don't be fooled, by the way, by products billed as "object-based" or by claims that "there are lots of ways to define an object." Translate this sort of marketing hype as follows: "Our product isn't really object-oriented -- we know that, but you probably don't, and your manager (who's making the purchase decision) almost certainly doesn't -- so we'll throw up a smoke screen and hope nobody notices." In the case of Microsoft, it has simply redefined object-oriented to mean something that fits with its product line. Visual Basic, for example, isn't in the least bit object-oriented. Neither is Microsoft Foundation Classes (MFC) or most of the other Microsoft technology that claims to be object-oriented. (How many Microsoft programmers does it take to screw in a light bulb? None. Let's define darkness as the new industry standard.)
All the rules in the rule-of-thumb list above essentially say the same thing -- that the inner state of an object must be hidden. In fact, the last rule in the list ("All objects must provide their own UI") really just follows from the others. If access to the inner state of an object is impossible, then the UI, which by necessity must access the state information, must be created by the object whose state is being displayed. The remainder of this article, as well as the columns of the next few months, will explain how you can make this work.
Procedural vs. object-oriented systems
The main reason to heed the rules in the previous section is that they make your code easier to maintain, because all the changes that typically need to be done to fix a problem or add a feature tend to be concentrated in one place. Don't confuse ease of maintenance with lack of complexity, by the way. Object-oriented systems are usually more complex than procedural systems but are easier to maintain. The idea is to organize the inevitable complexity inherent in real computer programs, not to eliminate it. Object-oriented designers, as a class, consider the elimination of complexity to be an impossible goal. They strive to organize the inherent complexity of a complex system in such a way that the system is manageable. If anything, good object-oriented systems are more complex than procedural ones, but in such systems the program is better organized and thus easier to maintain.
Compare and contrast: An example
Consider a system designed to get names from users. You might be tempted to use a
TextField from which you extract a
String, but that just won't work in a robust application. What if the system needs to run in China? (Unicode comes nowhere near representing all the idiographs that comprise written Chinese.) What if a user wants to enter a name using a pen (or speech recognition) rather than a keyboard? What if the database you're using to store the names can't store Unicode? What if you need to change the program a year from now to add employee IDs everywhere names are entered or displayed? In a procedural system, the solutions you come up with to answer these questions usually highlight enormous maintenance problems inherent to these systems. There's just no easy way to solve even the smallest problem, and a vast effort is often required to make simple changes.
An object-oriented solution tries to encapsulate those things that are likely to change in such a way that a change to one part of the program won't impact the rest of the program at all. For example, an object-oriented solution to the problems I just discussed requires a
Name class, objects of which know how to both display and initialize themselves. You would display the name by saying "display yourself over there," passing in a
Graphics object, or perhaps a
Container to which the name could drop in a
JPanel that displayed the name. You would create a UI for a name by telling an empty
Name object to "initialize yourself using this piece of this window." The
Name object might choose to create a
TextField for this purpose, but that's its business. You, as a programmer, simply don't care how the name goes about initializing itself, as long as it is initialized. (The implementation might not create a UI at all -- it might get the initial value by getting the required information from a database or from across a network.)
Non-object-oriented approach #1: Model-View-Controller
Contrast the approach in the example above with the way a UI generated by a system like Visual Café (or J++, or VisualAge, or JBuilder, or any of the rest) might work. On the output side, a
Frame class would extract data from a set of objects and render that data on its client window. On the input side, a
Frame might extract data from a bunch of underlying widgets (or controls, or JavaBeans, or whatever you want to call them) and then pass that data into the objects that comprise the logical model by calling a bunch of "set" functions. This architecture is known as Model-View-Controller (MVC). The widgets comprise the "view"; the
Frame is the "controller"; and the underlying system is the "model." MVC is okay for implementing little things like buttons, but it fails miserably as an application-level architecture.
This extract-data-then-shove-it-elsewhere approach requires you to know way too much about how the model-level objects are implemented. A system based on that approach cannot be called object-oriented: there's just too much data flowing around for the system to be maintainable. Unfortunately, many programmers mimic the MVC architecture in their own hand-built code, so this non-object-oriented design is endemic in some programs.
Non-object-oriented approach #2: Rapid application development
Rather than take my word for it, let's explore a few of the maintenance problems that arise when you try to develop a significant program using the MVC architecture. In the simple example above, you're tasked with adding an employee ID to every name in every screen that displays employee names. In the RAD-style architecture, you'll have to modify every one of these screens by hand, modifying or adding widgets to accomodate the new ID field. You'll also have to add facilities to the
Employee class to be able to set the ID. And you'll have to examine every class that uses an
Employee to ensure that the ID hasn't broken anything. For example, comparison of two
Employee objects to see if they're equal must now use the
ID, so you now have to modify all this code. If you had simply encapsulated the identity into a
Name class, none of this work would be necessary. The
Name objects would simply display themselves in the new way. Two
Name objects would now compare themselves using the ID information; your code that called
fred.equals(ginger) wouldn't have to change at all.
You can't even automate the update-the-code process, because all that WYSIWYG functionality (that's so often touted in marketing buzz) hides the code-generation process to the extent that it's impossible to modify machine-generated code safely. In any event, if you automatically modify machine-generated code, your modifications will be blown away the next time somebody uses the visual tool. Even if you don't use the tool again, modifying machine-generated code is always risky, since most of the RAD tools are very picky about what this code looks like, and if you do something unexpected in your modifications, the RAD tool is likely to become so confused that it will refuse to do anything at all the next time you do need to use it. Moreover, this machine-generated code is often miserable stuff, created with little thought given to efficiency, compactness, readability, and other important issues.
The real abomination in the RAD-style architecture is the "data-bound grid control," a table-like widget that effectively encapsulates the SQL that's needed to fill its cells from a database. What happens when the underlying data dictionary changes? All this embedded SQL is rendered meaningless. You'll have to search out every screen in the system that has a data-bound control and change that screen using a visual tool. Going to a "three-tier" system -- where the UI layer talks to a layer that encapsulates the SQL, which in turn talks to the database -- does nothing but make the problem worse, since the code you have to modify has been distributed out into more places. And in any event, if the middle tier is made of machine-generated code (usually the case), its very existence is of little use from a maintenance point of view.
All this modifying-every-screen-by-hand business is way too much work for me. Any time savings you may have gained by using the RAD tool to produce the initial code are more than lost as soon as the code hits maintenance.
The appeal of these systems often lies in familiarity. They help you program in an unfamiliar object-oriented language using a familiar procedural mindset. This sort of I-can-program-FORTRAN-in-any-language mindset, however, precludes your ability to leverage the real maintenance benefits of object-oriented systems. I personally feel that there's absolutely no reason to use Java (or C++) unless you are indeed implementing an object-oriented design. C++ in particular has so many problems that it's not worth messing with if you aren't going to take advantage of its strengths, and even Java isn't the height of simplicity -- it's simple only when compared to C++. You're better off just using C if you want to write procedural systems.
On the other hand, if you are doing an object-oriented design, a language designed to implement object-oriented systems (like Java) can make the implementation dramatically easier. Many C programmers try to program in Java as if they were programming in C, however, implementing procedural systems in Java rather than object-oriented systems -- in other words, writing really awful code. This practice is really encouraged by the language, which unfortunately mimics much of the syntax of C and C++, including flaws like the messed-up precedence of bitwise operators. Java mitigates the situation a bit because it's more of a "pure" object-oriented language than C++. It is therefore harder, though not impossible, to abuse. (A determined individual can write garbage code in any language.)
The well-crafted object-oriented system
Because the object-oriented way of looking at things is both essential and unfamiliar, let's look at a more involved example of both the wrong way and the right way to put together a system from the perspective of an object-oriented designer. I'll use an ATM machine for this example (as do many books), not because any of us will be implementing ATMs, but because an ATM is a good analog for both object-oriented and client/server architectures. Look at the central bank computer as a server object and the ATM as a client object.
A procedural solution
Most procedural database programmers would look at the server as a repository of data and the client as a requester of that data. Such a programmer might approach the problem of an ATM transaction as follows:
- The user walks up to a machine, inserts the card, and punches in his or her PIN.
- The ATM formulates a query of the form "give me the PIN associated with this card," sends the query to the database, and verifies that the returned value matches the one provided by the user. The ATM sends the PIN to the server as a string -- part of the SQL query -- but the returned number is stored in a 16-bit int to make the comparison easier.
- The user requests a withdrawal.
- The ATM formulates another query, this time: "give me the account balance." It stores the returned balance in a 32-bit float.
- If the balance is large enough, the machine dispenses the cash, and then posts an "update the balance for this user" to the server.
So what's wrong with this picture? Let's start with the returned balance. What happens when Bill Gates walks into our bank wanting to open a non-interest-bearing checking account and put all his money in it? We really don't want to send him away, but the last time we looked, Bill was worth about 87.94 gigabucks (see Resources). Unfortunately, the 32-bit float we're using for the account balance can represent at most 20 megabucks (4 gigabucks, divided by 2 for the sign bit, divided by 100 for the cents). Similarly, the 16-bit int used for the PIN can hold at most four decimal digits. What if Bill wants to use "GATES" (five digits) for his PIN? The final issue is that the SQL queries are formulated by the ATM. If the underlying data dictionary changes (if the name of a field changes, for example), the SQL queries won't work any more.
The procedural solution to all these problems is to change the ROMs in every ATM in the world (since there's no telling which one Bill will use) to use 64-bit doubles instead of 32-bit floats to hold account balances and to 32-bit longs to hold five-digit PINs. That's an enormous maintenance problem, of course.
Stepping into the real world for a moment, the cost of software deployment is one of the largest line-items on an IT department's budget. One of the appeals of Java (and the NC architecture, which is still alive and well) in an enterprise-level company is that, by eliminating deployment costs, you can save hundreds of millions of dollars every year. The client/sever equivalent of "swapping all the ROMs" -- deploying new versions of the client-side applications -- is a big deal. Similar maintenance problems exist inside most procedural programs, even those that don't use databases. Change definitions of a few central data types or global variables (that is, the program's equivalent of the data dictionary), and virtually every subroutine in the program might have to be rewritten. It's exactly this sort of maintenance nightmare that object-oriented design hopes to solve.
An object-oriented solution
To see how an object-oriented point of view can solve the problems I've just recited, let's recast the earlier problem in an object-oriented way, by looking at the system as a set of cooperating objects that have certain capabilities. The first step in any object-oriented design is to formulate a problem statement, which presents the problem we're trying to solve entirely in what's called the problem domain. In the current situation, the problem domain is banking. A problem statement describes a problem, not a computer program. I might describe the current problem as follows:
A customer walks into a bank, gets a withdrawal slip from the teller, and fills it out. The customer then returns to the teller, identifies himself, and hands the teller the withdrawal slip. (The teller verifies that the customer is who he says he his by consulting the bank records.) The teller then obtains an authorization from a bank officer and dispenses the money to the customer.
Armed with this simple problem statement, we can identify a few potential key abstractions (classes) and their associated operations. I'll use Ward Cunningham's CRC-Card format (see Resources for more information on CRC cards):
|Bank records||Creates withdrawal slips; verifies that customers are who they say they are||Teller: requests empty withdrawal slip|
|Bank officer||Authorizes withdrawals||Teller: requests authorization|
|Withdrawal slip||Records the amount of money requested by the teller|
Bank records: creates it
Bank officer: authorizes the withdrawal
Teller: presents it to customer
|Teller||Gets withdrawal slips from the bank records and routes the withdrawal slip to the bank officer for authorization|
Bank records: creates withdrawal slips
Bank officer: authorizes transactions
The server, in this model, is really the bank officer, whose main role is to authorize transactions. The
Bank, which is properly a server-side object as well, creates empty deposit slips when asked. The client side is represented by the
Teller object, whose main role is to get a deposit slip from the
Bank and pass it on. Interestingly, the customer (Bill) is external to the system, so he doesn't show up in the model. (Banks certainly have customers, but the customer isn't an attribute of the bank any more than the janitorial service is part of the bank. The customer's accounts could be attributes, certainly, but not the actual customers. You, for example, do not define yourself as a piece of your bank.)
An object-oriented ATM system just models the earlier problem statement. Here's the message flow:
- Bill walks up to an ATM, presents his card and PIN, and requests a withdrawal.
Tellerobject asks the server-side
Bank_recordsobject, "Is the person with this card and this PIN legit?"
Bank_recordsobject comes back with "yes" or "no."
Tellerobject asks the
Bank_recordsobject for an empty
Withdrawal_slip. (This object will be an instance of some class that implements the
Withdrawal_slipinterface, and will be passed from the
Bank_recordsobject to the
Tellerobject by value (using RMI). That's important. All the
Tellerknows about the object is the interface it implements -- the implementation (the class file) comes across the wire along with the object itself, so the
Tellerhas no way of determining how the object will actually process the messages sent to it. This abstraction is a good thing because it lets us change the way the
Withdrawal_slipobject works without having to change the
Tellerobject tells the
Withdrawal_slipobject to display a user interface. (The object complies by rendering a UI on the ATM screen using AWT.)
- Bill fills in the withdrawal slip.
Tellerobject notices that the initialize-yourself operation is complete (perhaps by monitoring the OK key), and passes the filled-out
Withdrawal_slipobject to the server-side
Bank_officerobject (again by value, using RMI) as an argument to the message: "Am I authorized to dispense this much money?"
- The server-side
Bank_officerobject comes back with "yes" or "no."
- If the answer is yes, the ATM dispenses the money. (For the sake of simplicity, I won't go into how that happens.)
Of course, this isn't the only (or even the ideal) way to do things, but the example gets the idea across -- bear with me.
The main thing to notice in this second protocol is that all knowledge of how a balance or PIN is stored, how the server decides whether or not it's okay to dispense money, and so forth, is hidden inside the various objects. This is possible because the server is now an object that implements the "authorization" capability. Rather than requesting the data that we need to authorize a transaction, the system asks the (server-side)
Bank_officer object (which has the data) to do the work for us. No data (account balances or PINs) is being shipped to the ATM, so there's no need to change the ATM when the server code changes. Also note that the
Teller object isn't even aware of how the money is specified. That is, the requested withdrawal amount is encapsulated entirely within the
Withdrawal_slip object. Consequently, a server-side change in the way money is represented is entirely transparent to the client-side
Teller. And, most importantly, the bank's maintenance manager is happily sleeping it off in the back office instead of running around changing ROMs. If only ATMs had been written this way in Europe, translation to the euro would have been a simple matter of changing the definition of the
Money) class on the server side. Subsequent requests for a
Withdrawal_slip from an ATM would yield a euro-enabled version in reply.
Working with legacy systems
Another important issue is the "objectification" of legacy systems. In general, the differences in organization between procedural and legacy systems is so extreme that the vast majority of the code can't be salvaged. You're in pretty good shape if your procedural system can be modularized into subsystems that can talk to one another over a well-defined interface. Each of these subsystems can be viewed as a "facade" object, and the subsystems can be translated to an object-oriented implementation independently of one another. Most legacy systems are not so well organized, however, and the translation can't even begin until you impose a modular organization at the procedural level. Your best bet is to design a new system from scratch, and then figure out how to salvage as much of the existing code as possible while implementing the new system. It's impossible to transform a procedural system into an object-oriented system without a complete object-oriented design to show you the way.
One of the most common problems that arise when making this translation is a misinformed attempt to turn an existing struct definition into a class by making the data private and then providing a
set function for each field. This isn't object-orientated programming -- it's just a very complicated way to access a field that could be more easily accessed with a dot. In an object-oriented system, the data wouldn't be accessed at all by nonmembers of the class. Rather, messages sent to an object would request that the object exercise some capability, and the message handler might use the data members of the class to do its job. Remember, structs and classes are very different at the design level. A struct is a collection of data, a class is a collection of capabilities.
A failed attempt to translate a legacy system
I saw a good example of this problem: a human-resources package that the programmers were attempting to translate to be object-oriented. They took an existing employee record (a C struct) and tried to transmogrify it into an
Employee class by making the fields private and providing
set() methods for each field. One problem is that an
Employee class will almost certainly have a salary attribute, and, unfortunately, the original employee record had a salary field, so the programmers equated the two. An attribute isn't the same as a field, however. An attribute is a defining characteristic of an object. A salary serves to differentiate an employee from a generic "person," for example. Without the salary, there would be no difference between an employee and a person. Moreover, an attribute doesn't necessarily map to a field in the class. The salary might be stored in an external database, for example, with the
Employee storing only the information needed to retrieve the attribute from the database. If the
Employee does store the salary internally, it might be stored in a float, a double, a binary-coded-decimal array, a string -- there's no telling. What, then, could a
get_salary() function return? One of the main tenets of object-oriented design is that it should be possible to radically modify the private components of a class definition without affecting the users of the class at all. The salary might be a float today, but there's no guarantee that it will stay that way. Similarly, the matching
get_salary() method might return a float today, but what if the internal representation changed in such a way that a float wouldn't work anymore? Say, for example, you needed to return an object of type
Money that worked like a float but handled the round-off-error-on-pennies problem. Letting the function continue to return a float while using the
Money class internally would defeat the purpose of the
Money class. Changing the function's return value would break every subroutine that called the function.
It's exactly this change-induced ripple effect that object-oriented techniques are meant to avoid.
The programmers of this system should have decided up front what capabilities to give the
Employee object -- what it could do, not what fields it had. Put another way, you should never access an attribute directly; rather, you should ask the object to do something with its attributes for you. In the case of the
Employee object, there should be no message of the form "give me your salary." You shouldn't extract the attribute from an object in order to do something with it (like draw it in a window). Rather, you should tell the object itself to do the operation ("print your salary in this window"). The same reasoning would apply to a name -- not "give me your name," but "draw your name here." This means that the way the attribute is stored inside the object is utterly irrelevant. And as long as the salary is printed, I don't care how it's stored.
Though the foregoing process is the preferred way of doing things, you do sometimes need to extract an attribute from an object. The salary might be needed by a
Payroll_department class to generate a paycheck, for example, and it might not be appropriate for an
Employee object to control the amount of its own paycheck. There are several solutions to this problem, probably the best would be for the
Payroll_department class to ask the
Employee for its salary. The salary should be encapsulated in a
Money object, and the
Salary object should implement all the capabilities needed to compute a paycheck (such as "divide yourself by," "subtract this from yourself," and of course, "draw yourself in this window"). This way, the representation of the salary is still hidden. The returned
Salary object should, of course, be a constant. You don't want a
Payroll_department to be able to modify a salary, and you don't want the exposed salary to be different from the one stored in the
This sort of encapsulation should really be used at every level of the system, even for such basic things as strings. A
String class really shouldn't expose any information about how the characters are stored internally. In Java, the presence of a
getBytes() method in the
String is a design flaw. All the operations you need for strings should be implemented as methods of the
String class. So it shouldn't be "give me your buffer so I can do something to it," but "do something to yourself." This way, you're completely isolated from the way that the
String objects store characters internally. The characters could be stored in a
char array, but a
String could just as easily represent itself internally using some sort of multibyte coding, or even a sequence of images, or some form that no one has thought of yet. If the internal representation is never exported, the implementation of the internal representation is irrelevant to the user of the
This structure implies, of course, that there are no functions in the system that take
byte (or even
char) arguments; all strings must be
String objects. And that's another aspect of object-oriented systems: they tend to be all-or-nothing. Everything must be a
String object, there's no way to mix string objects with arrays of
char and get the maintenance advantage promised by object orientation. That's one of the reasons that the translation of legacy systems to object-oriented systems is so difficult.
There are two ways to initialize a string in a GUI. You could pass the
String object the message: "Initialize yourself dynamically, using this piece of this window." The
String would then create the
JTextField needed for that purpose and install it on the window. Or you could say "Give me back some
Component object that will be used for initialization." I think of this
Component object as a visual proxy for some hidden field of the
String. I can position it on the screen, size it, enable and disable it, and so on. The characters the user types into the proxy flow directly into the
String object that created it, however. (The
String object sets itself up as a listener to the
TextField object used as a proxy before it returns the proxy to the requester.) The
Frame that holds the proxy isn't aware that any activity has occurred at all. I'll discuss this visual-proxy architecture in more depth next month.
The point of this organization is that you can now make changes to the structure of an object without affecting any of the code that uses the object -- and that's one of the main strengths of an object-oriented approach. Changes made in one place are highly localized (as are bugs). This localization not only makes code easier to maintain, it makes it easier to debug and to write initially.
The obvious 'object'ions (so to speak)
Whenever I present the foregoing, I always get a few knee-jerk responses from the hard-core procedural element in the crowd, so let me address a few of the more common gripes up front.
Gripe #1. Graphical functionality within an object
One of the main objections I've heard to the object-oriented approach to UI development is: "Horrors! Then there's graphical functionality inside model-level objects." Well, not exactly, but even if there were, why is that a problem? The next few columns will describe in detail how to actually separate the graphical functionality from the model in an object-oriented way using variants on the visual-proxy pattern I just introduced to concentrate the UI mechanisms into well-defined places.
For now, though, chew on the following. Traditional procedural programs attempted to isolate the underlying graphical system from the logical parts of the program (the model) by concentrating all system-dependent code in one subsystem. The goal was to be able to change graphical environments, for example, by swapping out a single subsystem. Historically, this approach has failed miserably in the procedural world because myriad tentacles tend to go from the UI side to the model side. In other words, the GUI is concentrated into a single shared library or DLL, but it isn't isolated from the rest of the system in any meaningful way. Every time the model changes, so does the UI, and vice versa. Not only have you solved nothing by "isolating" the GUI, you've introduced a considerable amount of unnecessary complexity, and the concomitant maintenance difficulties, into the program.
Just because we've historically done things in a particular way doesn't mean it is the best way to do things; otherwise, we'd all still be programming in assembly language. Conventional wisdom is rarely correct. I want proof. In any event, the architecture I'm advocating is isolated from the underlying system, in spite of the fact that there's some model-side rendering going on. You aren't writing directly to the OS; you're writing to AWT (or Swing), which is after all the Abstract Windowing Toolkit. That is, by writing to an abstraction layer, you have isolated yourself from the graphical environment. If you change graphical environments, all the necessary changes are concentrated in one place: inside AWT and Swing.
Gripe #2. Different views into the same object
The other bugaboo that I want to put to death is the notion of different views into the same object, usually characterized by the question: "Suppose you need to display this data as a pie chart over here and a grid over there? How can you do this if the object displays itself?"
Now let's get real. How often in your work has this problem actually come up? In talking about object-oriented architectures for the user interface to many hundreds, if not thousands, of programmers, only two or three have ever raised their hands when I asked that question. If I need a generic presentation program that has no notion of what the data means, I'll go buy a copy of Excel or Quattro Pro. I won't write a program. The fact is that data has meaning -- it's not just an arbitrary collection of numbers. For a given set of data, I would argue that that there is only one "best" way to represent it for a specific problem domain. If there's no "best" way, then just settle on one "good" way. This degree of flexibility is rarely required. In any event, it is possible for an object to display itself in different ways without violating its integrity. I'll talk about how to do just that in a forthcoming column.
The next question is usually: "What about JavaBeans, they use
set functions all over the place." First, these
set functions are intended to provide an interface between the bean and its (compile-time) design tool. In an object-oriented system, you'll never call one of these
set methods from your own code (or at least you shouldn't). In an ideal world, Java would have a
bean access privilege that would work like
private but would permit access by the classes that comprise a
BeanBox tool as well. In any event, this
set strategy was provided in an (I think misguided) attempt to make programming beans "easy" -- that is, easy for procedural programmers who don't know object-oriented programming. Fortunately, the JavaBeans spec does provide a true object-oriented mechanism for interacting with the design-time tool: the bean
Customizer, which creates a complete UI in response to an "initialize yourself" request. A tool-generated property sheet isn't used if you have a
Customizer. If you're serious about object-oriented programming, then your beans should all initialize themselves with
Customizers, and you shouldn't use the
set strategy at all.
Most 'RAD' tools don't make things go faster
Finally, I want to shoot off at the mouth a bit about the whole notion of "rapid" application development tools. If you haven't figured it out by now, I'm not much of a fan of the current crop of programs that go by that name. The original RAD systems weren't wowie-zowie software programs; they were very structured design and development processes. The way most developers work with the RAD tools is inimical to fundamental notions of the RAD process. Real RAD always involves a lengthy and thorough design process, for example. Today's typical RAD tool, on the other hand, makes it almost impossible to do real design work, since they look at a system as a UI with a few intelligent warts hanging off of it. There's no coherence to the resulting code -- in many systems you can't even print a complete listing. Even if you do do a complete design, most RAD tools provide you with no way of realizing it since they force you into their notion of what the system should look like. In other words, you're forced by the system into supporting the RAD tools' usually horrendous architecture.
So why are the RAD systems useless in producing object-oriented user interfaces? The short answer is, because they all use the extract-data-then-shove-it-elsewhere approach I discussed earlier. Any system whose design focuses on data flow, rather than message passing, is fundamentally incorrect in the object-oriented model.
As a consequence, RAD tools deliver virtually none of their promise. That application you throw together so quickly is going to take ten to twenty times more effort to maintain than would hand-built code. To paraphrase Fred Brook's wonderful essay "No Silver Bullet," well over half of the time you spend working on a project (on the order of 70 percent) is spent thinking, and no tool, no matter how advanced, can think for you. Consequently, even if a tool did everything except the thinking for you -- if it wrote 100 percent of the code, wrote 100 percent of the documentation, did 100 percent of the testing, burned the CD-ROMs, put them in boxes, and mailed them to your customers -- the best you could hope for would be a 30 percent improvement in productivity. In order to do better than that, you have to change the way you think.
So, changing the way that you think -- in an applied sense --- will be the subject of the next few articles. Rather than discussing object-oriented priniciples as an academic exercise, we'll build a few UI tools using the principles I've discussed here. The forms-based I/O package that we'll start on next month, for example, sets things up so that the implementation of the model-side objects is completely hidden from the display code. This way, you can radically change the way your forms look without impacting the model, and you can radically change the implementation of the model without impacting the forms-generation code. This sort of neat modularization in aid of easy maintenance is what object-oriented design is all about. We'll also see, however, that drag-and-drop tools will be of no help at all in building this object-oriented system. On the flip-side, we'll also see that the code is simple enough to construct by hand so that a drag-and-drop tool isn't all that necessary.
Learn more about this topic
- The Bill Gates Personal Wealth Clock. Find out what Bill's really worth
- "No Sliver Bullet," The Mythical Man Month, Anniversary Edition, Frederick Brooks (Addison-Wesley, 1995)
- The CRC Card Book, David Bellin and Susan Suchman Simone (Addison-Wesley, 1997)
- C++ in Hypertext, Curtis Solohub. This online CS-1 textbook offers a pretty good description of object-oriented-design techniques in general (and CRC cards in particular)