Building user interfaces for object

advertisement
Building user interfaces for
object-oriented systems, Part 1
What is an object? The theory behind building objectoriented user interfaces
Summary
This month's article, the first of a series on user-interface construction in object-oriented
systems, looks at the underlying theory of object-oriented design from the perspective of
the user interface. This installment in the new series serves as a primer on objects,
describing exactly what a well-constructed object is -- and isn't -- and how it should behave
with respect to the system as a whole. The author also discusses why the rapid application
development (RAD) tools like Visual Café and Visual J++ aren't particularly useful in an
object-oriented environment. A thorough understanding of the underpinnings of objectoriented programming is essential to understanding the user interface solutions you'll be
building over the next few months. (9,000 words)
By Allen Holub
his 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
Part
Part
Part
Part
Part
1.
2.
3.
4.
5.
6.
What is an object? The theory behind building object-oriented user interfaces
The visual-proxy architecture
The incredible transmogrifying widget
Menu negotiation
Useful stuff
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 objectoriented 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-toknow-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:
1. All data is private. Period. (This rule applies to all implementation details, not just
the data.)
2. get and set functions are evil. (They're just elaborate ways to make the data
public.)
3. 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.
4. 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.
5. 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 objectoriented 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 objectoriented; 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.compareTo(ginger) or
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 tablelike 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-programFORTRAN-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:
1. The user walks up to a machine, inserts the card, and punches in his or her PIN.
2. 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.
3. The user requests a withdrawal.
4. The ATM formulates another query, this time: "give me the account balance." It
stores the returned balance in a 32-bit float.
5. 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 clientside 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):
Class
Responsibility </STRONG< FONT>
Collaborates with
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 Bank records: creates
withdrawal slip to the bank officer for authorization
withdrawal slips
Bank officer: authorizes
transactions
</STRONG<
FONT>
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:
1. Bill walks up to an ATM, presents his card and PIN, and requests a withdrawal.
2. The Teller object asks the server-side Bank_records object, "Is the person with
this card and this PIN legit?"
3. The Bank_records object comes back with "yes" or "no."
4. The Teller object asks the Bank_records object for an empty
Withdrawal_slip. (This object will be an instance of some class that implements
the Withdrawal_slip interface, and will be passed from the Bank_records object
to the Teller object by value (using RMI). That's important. All the Teller knows
about the object is the interface it implements -- the implementation (the class file)
comes across the wire along with the object itself, so the Teller has 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_slip
object works without having to change the Teller definition.)
5. The Teller object tells the Withdrawal_slip object to display a user interface.
(The object complies by rendering a UI on the ATM screen using AWT.)
6. Bill fills in the withdrawal slip.
7. The Teller object notices that the initialize-yourself operation is complete (perhaps
by monitoring the OK key), and passes the filled-out Withdrawal_slip object to
the server-side Bank_officer object (again by value, using RMI) as an argument
to the message: "Am I authorized to dispense this much money?"
8. The server-side Bank_officer object comes back with "yes" or "no."
9. 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 Withdrawal_slip
(or 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 get and 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 get() and 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 objectoriented 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 Salary or
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 Employee.
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 String.
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 hardcore 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 get and set functions all
over the place." First, these get and 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 get or 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 get/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 objectoriented 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 get/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.
Conclusion
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 formsgeneration code. This sort of neat modularization in aid of easy maintenance is what objectoriented 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.
About the author
Allen Holub has been working in the computer industry since 1979. He is widely published in
magazines (Dr. Dobb's Journal, Programmers Journal, Byte, and MSJ, among others). He
has seven books to his credit, and is currently working on an eighth that will present the
complete sources for a Java compiler written in Java. After eight years as a C++
programmer, Allen abandoned C++ for Java in early 1996. He now looks at C++ as a bad
dream, the memory of which is mercifully fading. He's been teaching programming (first C,
then C++ and MFC, now object-oriented design and Java) both on his own and for the
University of California Berkeley Extension since 1982. Allen offers both public classes and
in-house training in Java and object-oriented design topics. He also does object-oriented
design consulting and contract Java programming. Get information, and contact Allen, via
his Web site http://www.holub.com.
Resources


The Bill Gates Personal Wealth Clock. Find out what Bill's really worth:
http://www.webho.com/WealthClock
"No Sliver Bullet," The Mythical Man Month, Anniversary Edition, Frederick Brooks
(Addison-Wesley, 1995):
http://www1.fatbrain.com/asp/bookinfo/bookinfo.asp?theisbn=0201835959


The CRC Card Book, David Bellin and Susan Suchman Simone (Addison-Wesley,
1997):
http://www1.fatbrain.com/asp/bookinfo/bookinfo.asp?theisbn=0201895358
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):
http://cs.nmhu.edu/personal/curtis/cs1htmlfiles/Cs1text.htm
Download