Building user interfaces for object-oriented systems, Part 1

What is an object? The theory behind building object-oriented user interfaces

1 2 3 4 Page 4
Page 4 of 4

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 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):

ClassResponsibilityCollaborates with
Bank recordsCreates withdrawal slips; verifies that customers are who they say they areTeller: requests empty withdrawal slip
Bank officerAuthorizes withdrawalsTeller: requests authorization
Withdrawal slipRecords the amount of money requested by the teller

Bank records: creates it

Bank officer: authorizes the withdrawal

Teller: presents it to customer

TellerGets 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:

  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 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 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 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 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 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 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 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.

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.

Learn more about this topic

Related:
1 2 3 4 Page 4
Page 4 of 4