Top level objects, abstractions and metaphors, components.
It is all too easy to start the design by coming up with such objects as hash tables, linked lists, queues, trees, and trying to put them together. Such an approach, bottom-up, implementation driven, should be avoided. A program that is built bottom-up ends up with a structure of a soup of objects. There are pieces of vegetables, chunks of meat, noodles of various kinds, all floating in some kind of broth. It sort of looks object oriented--there are "classes" of noodles, vegetables, meat, etc. However, since you rarely change the implementation of linked lists, queues, trees, etc., you don't gain much from their object-orientedness. Most of the time you have to maintain and modify the shapeless soup.
Using the top-down approach, on the other hand, you divide your program into a small number of interacting high-level objects. The idea is to deal with only a few types of objects--classes (on the order of seven plus/minus two--the capacity of our short-term memory!). The top-level objects are divided into the main actors of the program, and the communication objects that are exchanged between the actors. If the program is interactive, you should start with the user interface and the objects that deal with user input and screen- (or teletype-) output.
Once the top level objects are specified, you should go through the exercise of rehearsing the interactions between the objects (this is sometimes called going through use-case scenarios). Go through the initialization process, decide which object have to be constructed first, and in what state they should start. You should avoid using global objects at any level other than possibly the top level. After everything has been initialized, pretend that you are the user, see how the objects react to user input, how they change their state, what kind of communication objects they exchange. You should be able to describe the interaction at the top without having to resort to the details of the implementation of lower levels.
After this exercise you should have a pretty good idea about the interfaces of your top-level objects and the contracts they have to fulfill (that is, what the results of a given call with particular arguments should be). Every object should be clearly defined in as few words as possible, its functionality should form a coherent and well rounded abstraction. Try to use common language, rather than code, in your documentation, in order to describe objects and their interactions. Remember, center the project around humans, not computers. If something can be easily described in common language, it usually is a good abstraction.
For things that are not easily abstracted use a metaphor. An editor might use a metaphor of a sheet of paper; a scheduler a metaphor of a calendar; a drawing program, metaphors of pencils, brushes, erasers, palettes, etc. The design of the user interface revolves around metaphors, but they also come in handy at other levels of design. Files, streams, semaphores, ports, pages of virtual memory, trees, stacks--these are all examples of very useful low-level metaphors.
The right choice of abstractions is always important, but it becomes absolutely crucial in the case of a large software project, where top-level objects are implemented by separate teams. Such objects are called components. Any change to the component's interface or its contract, once the development started going full steam ahead, is a disaster. Depending on the number of components that use this particular interface, it can be a minor or a major disaster. The magnitude of such a disaster can only be measured in Richter scale. Every project goes through a few such "earthquakes"--that's just life!
Now, repeat the same design procedure with each of the top-level objects. Split it into sub-objects with well defined purpose and interface. If necessary, repeat the procedure for the sub-objects, and so on, until you have a pretty detailed design. Use this procedure again and again during the implementation of various pieces. The goal is to superimpose some sort of a self-similar, fractal structure on the project. The top level description of the whole program should be similar to the description of each of the components, its sub-components, objects, sub-objects, etc. Every time you zoom-in or zoom-out, you should see more or less the same type of picture, with a few self-contained objects collaborating towards implementing some well-defined functionality.
Designing user interface, input driven programs, Model-View-Controller paradigm
Even the simplest modern-day programs offer some kind of interactivity. Of course, one can still see a few remnants of the grand UNIX paradigm, where every program was written to accept a one dimensional stream of characters from its standard input and spit out another stream at its standard output. But with the advent of the Graphical User Interface (GUI), the so-called "command-line interface" is quickly becoming extinct. For the user, it means friendlier, more natural interfaces; for the programmer it means more work and a change of philosophy.
With all the available help from the operating system and with appropriate tools at hand it isn't difficult to design and implement user interfaces, at least for graphically non-demanding programs. What is needed is a change of perspective. An interactive program is, for the most part, input-driven. Actions in the program happen in response to user input. At the highest level, an interactive program can be seen a series of event handlers for externally generated events. Every key press, every mouse click has to be handled appropriately.
The object-oriented response to the interactive challenge is the Model-View-Controller paradigm first developed and used in Smalltalk. The Controller object is the focus of all external (and sometimes internal as well) events. Its role is to interpret these events as much as is necessary to decide which of the program objects will have to handle them. Appropriate messages are then sent to such objects (in Smalltalk parlance; in C++ we just call appropriate methods).
The View takes care of the program's visual output. It translates requests from other objects into graphical representations and displays them. In other words it abstracts the output. Drawing lines, filling areas, writing text, showing the cursor, are some of the many responsibilities of the View.
Centralizing input in the Controller and output in the View leaves the rest of the program independent from the intricacies of the input/output system (also makes the program easy to port to other environments with slightly different graphical capabilities and interfaces). The part of the program that is independent of the details of input and output is called the Model. It is the hard worker and the brains of the program. In simple programs, the Model corresponds to a single object, but quite often it is a collection of top level objects. Various parts of the Model are activated by the Controller in response to external events. As a result of changes of state, the Model updates the View whenever it finds it appropriate.
As a rule, you should start the top-down design of an interactive program by establishing the functionality of the Controller and the View. Whatever happens prior to any user action is considered initialization of these components and the model itself. The M-V-C triad may also reappear at lower levels of the program to handle a particular type of control, a dialog box, an edit control, etc.
Statement of purpose, functionality, user interface, input, output, size limitations and performance goals, features, compatibility.
The first document to be written before any other work on a project may begin is the Requirement Specification (also called an External Specification). In large projects the Requirement Spec might be prepared by a dedicated group of people with access to market research, user feedback, user tests, etc. However, no matter who does it, there has to be a feedback loop going back from the architects and implementers to the group responsible for the Requirement Spec.
The crucial part of the spec is the statement of purpose--what the purpose of the particular software system is. Sometimes restating the purpose of the program might bring some new insights or change the focus of the design. For instance, describing a compiler as a program which checks the source file for errors, and which occasionally creates an object file (when there are no errors), might result in a competitively superior product.
The statement of purpose might also contain a discussion of the key metaphor(s) used in the program. An editor, for instance, may be described as a tool to manipulate lines of text. Experience however has shown that editors that use the metaphor of a sheet of paper are superior. Spreadsheet programs owe their popularity to another well chosen metaphor.
Then, a detailed description of the functionality of the program follows. In a word-processor requirement spec one would describe text input, ways of formatting paragraphs, creating styles, etc. In a symbolic manipulation program one would specify the kinds of symbolic objects and expressions that are to be handled, the various transformations that could be applied to them, etc. This part of the spec is supposed to tell the designers and the implementers what functionality to implement. Some of it is described as mandatory, some of it goes into the wish list.
The userinterface and visual metaphors go next. This part usually undergoes most extensive changes. When the first prototype of the interface is created, it goes through more or less (mostly less) rigorous testing, first by developers, then by potential users. Sometimes a manager doesn't like the feel of it and sends programmers back to the drawing board. It is definitely more art than science, yet a user interface may make or break a product.
What compounds the problem is the fact that anybody may criticize user interface. No special training is required. And everybody has different tastes. The programmers that implement it are probably the least qualified people to judge it. They are used to terse and cryptic interfaces of their programming tools, grep, make, link, Emax or vi. In any case, designing user interface is the most frustrating and ungrateful job.
Behind the user interface is the input/output specification. It describes what kind of input is accepted by the program, and what output is generated by the program in response to this input. For instance, what is supposed to happen when the user clicks on the format-brush button and then clicks on a word or a paragraph in a document (the formatting should be pasted over). Or what happens when the program reads a file that contains a comma-separated lists of numbers. Or what happens when a picture is pasted from the clipboard.
Speed and size requirements may also be specified. The kind of processor, minimum memory configuration, and disk size are often given. Of course there is always conflict between the ever growing list of desired features and the always conservative hardware requirements and breathtaking performance requirements. (Features always win! Only when the project enters its critical phase, features get decimated.)
Finally, there may be some compatibility requirements. The product has to understand (or convert) files that were produced either by its earlier versions, or by competitors' products, or both. It is wise to include some compatibility features that will make future versions of the product easier to implement (version stamps are a must).
Top level view, crucial data structures and algorithms. Major implementation choices.
The architectural document describes how things work and why they work the way they work. It's a good idea to either describe theoretical foundations of the system, or at least give pointers to some literature.
This is the document that gives the top level view of the product as a program, as seen by the developers. All top level components and their interactions are described in some detail. The document should show it clearly that, if the top level components implement their functionality according to their specifications, the system will work correctly. That will take the burden off the shoulders of developers--they won't have to think about too many dependencies.
The architectural spec defines the major data structures, especially the persistent ones. The document then proceeds with describing major event scenarios and various states of the system.
The program may start with an empty slate, or it may load some history (documents, logs, persistent data structures). It may have to finish some transactions that were interrupted during the last session. It has to go through the initialization process and presumably get into some quiescent state.
External or internal events may cause some activity that transforms data structures and leads to state transitions. New states have to be described. In some cases the algorithms to be used during such activity are described as well. Too detailed a description of the implementation should be avoided. It becomes obsolete so quickly that it makes little sense to try to maintain it.
Once more we should remind ourselves that the documentation is a living thing. It should be written is such a way that it is easy to update. It has to have a sensible structure of its own, because we know that it will be changed many times during the implementation cycle. In fact it should be changed, and it is very important to keep it up-to-date and encourage fellow developers to look into it on a regular basis.
In the implementation cycle, there are times when it is necessary to put some flesh into the design of some important object that has only been sketched in the architectural spec. It is time to either expand the spec or write short essays on selected topics in the form of separate documents. In such essays one can describe non-trivial implementations, algorithms, data structures, programming notes, conventions, etc.