Bruce Eckel's Thinking in C++, 2nd Ed Contents | Prev | Next

Analysis and design

The object-oriented paradigm is a new and different way of thinking about programming and many folks have trouble at first knowing how to approach a project. Now that you know that everything is supposed to be an object, and as you learn to think more in an object-oriented style, you can begin to create “good” designs, ones that will take advantage of all the benefits that OOP has to offer.

A method (also often called a methodology) is a set of processes and heuristics used to break down the complexity of a programming problem. Many OOP methods have been formulated since the dawn of object-oriented programming, and this section will give you a feel for what you’re trying to accomplish when using a method.

Especially in OOP, methodology is a field of many experiments, so it is important to understand what problem the method is trying to solve before you consider adopting one. This is particularly true with C++, where the programming language itself is intended to reduce the complexity involved in expressing a program. This may in fact alleviate the need for ever-more-complex methodologies. Instead, simpler ones may suffice in C++ for a much larger class of problems than you could handle with simple methods for procedural languages.

It’s also important to realize that the term “methodology” is often too grand and promises too much. Whatever you do now when you design and write a program is a method. It may be your own method, and you may not be conscious of doing it, but it is a process you go through as you create. If it is an effective process, it may need only a small tune-up to work with C++. If you are not satisfied with your productivity and the way your programs turn out, you may want to consider adopting a formal method, or choosing pieces from among the many formal methods.

While you’re going through the development process, the most important issue is this: don’t get lost. It’s easy to do. Most of the analysis and design methods are intended to solve the largest of problems. Remember that most projects don’t fit into that category, so you can usually have successful analysis and design with a relatively small subset of what a method recommends. But some sort of process, no matter how limited, will generally get you on your way in a much better fashion than simply beginning to code.

It’s also easy to get stuck, to fall into “analysis paralysis,” where you feel like you can’t move forward because you haven’t nailed down every little detail at the current stage. Remember that, no matter how much analysis you do, there are some things about a system that won’t reveal themselves until design time, and more things that won’t reveal themselves until you’re coding, or not even until a program is up and running. Because of this, it’s critical to move fairly quickly through analysis and design to implement a test of the proposed system.

This point is worth emphasizing. Because of the history we’ve had with procedural languages, it is commendable that a team will want to proceed carefully and understand every minute detail before moving to design and implementation. Certainly, when creating a DBMS, it pays to understand a customer’s needs thoroughly. But a DBMS is in a class of problems that is very well-posed and well-understood. The class of programming problem discussed in this chapter is of the “wild-card” variety, where it isn’t simply re-forming a well-known solution, but instead involves one or more “wild-card factors” – elements where there is no well-understood previous solution, and where research is necessary. [10] Attempting to thoroughly analyze a wild-card problem before moving into design and implementation results in analysis paralysis because you don’t have enough information to solve this kind of problem during the analysis phase. Solving such a problem requires iteration through the whole cycle, and that requires risk-taking behavior (which makes sense, because you’re trying to do something new and the potential rewards are higher). It may seem like the risk is compounded by “rushing” into a preliminary implementation, but it can instead reduce the risk in a wild-card project because you’re finding out early whether a particular design is viable.

It’s often proposed that you “build one to throw away.” With OOP, you may still throw part of it away, but because code is encapsulated into classes, you will inevitably produce some useful class designs and develop some worthwhile ideas about the system design during the first iteration that do not need to be thrown away. Thus, the first rapid pass at a problem not only produces critical information for the next analysis, design, and implementation iteration, it also creates a code foundation for that iteration.

That said, if you’re looking at a methodology that contains tremendous detail and suggests many steps and documents, it’s still difficult to know when to stop. Keep in mind what you’re trying to discover:

  1. What are the objects? (How do you partition your project into its component parts?)
  2. What are their interfaces? (What messages do you need to be able to send to each object?)
If you come up with nothing more than the objects and their interfaces then you can write a program. For various reasons you might need more descriptions and documents than this, but you can’t really get away with any less.

The process can be undertaken in four phases, and a phase 0 which is just the initial commitment to using some kind of structure.

Phase 0: Make a plan

The first step is to decide what steps you’re going to have in your process. It sounds simple (in fact, all of this sounds simple) and yet people often don’t even get around to phase one before they start coding. If your plan is “let’s jump in and start coding,” fine. (Sometimes that’s appropriate when you have a well-understood problem.) At least agree that this is the plan.

You might also decide at this phase that some additional process structure is necessary but not the whole nine yards. Understandably enough, some programmers like to work in “vacation mode” in which no structure is imposed on the process of developing their work: “It will be done when it’s done.” This can be appealing for awhile, but I’ve found that having a few milestones along the way helps to focus and galvanize your efforts around those milestones instead of being stuck with the single goal of “finish the project.” In addition, it divides the project into more bite-sized pieces and make it seem less threatening (plus the milestones offer more opportunities for celebrating).

When I began to study story structure (so that I will someday write a novel) I was initially resistant to the idea of structure, feeling that when I wrote I simply let it flow onto the page. What I found was that when I wrote about computers the structure was simple enough so that I didn’t need to think much about it, but I was still structuring my work, albeit only semi-consciously in my head. So even if you think that your plan is to just start coding, you still go through the following phases while asking and answering certain questions.

The mission statement

Any system you build, no matter how complicated, has a fundamental purpose, the business that it’s in, the basic need that it satisfies. If you can look past the user interface, the hardware- or system-specific details, the coding algorithms and the efficiency problems, you will eventually find the core of its being, simple and straightforward. Like the so-called high concept from a Hollywood movie, you can describe it in one or two sentences. This pure description is the starting point.

The high concept is quite important because it sets the tone for your project; it’s a mission statement. You won’t necessarily get it right the first time (you may be in a later phase of the project before it becomes completely clear), but keep trying until it feels right. For example, in an air-traffic control system you may start out with a high concept focused on the system that you’re building: “The tower program keeps track of the aircraft.” But consider what happens when you shrink the system to a very small airfield; perhaps there’s only a human controller or none at all. A more useful model won’t concern the solution you’re creating as much as it describes the problem: “Aircraft arrive, unload, service and reload, and depart.”

Phase 1: What are we making?

In the previous generation of program design (called procedural design ), this is called “creating the requirements analysis and system specification .” These, of course, were places to get lost: intimidatingly-named documents that could become big projects in their own right. Their intention was good, however. The requirements analysis says “Make a list of the guidelines we will use to know when the job is done and the customer is satisfied.” The system specification says “Here’s a description of what the program will do (not how) to satisfy the requirements.” The requirements analysis is really a contract between you and the customer (even if the customer works within your company or is some other object or system). The system specification is a top-level exploration into the problem and in some sense a discovery of whether it can be done and how long it will take. Since both of these will require consensus among people, I think it’s best to keep them as bare as possible – ideally, to lists and basic diagrams – to save time. You might have other constraints that require you to expand them into bigger documents, but by keeping the initial document small and concise, it can be created in a few sessions of group brainstorming with a leader who dynamically creates the description. This not only solicits input from everyone, it also fosters initial buy-in and agreement by everyone on the team. Perhaps most importantly, it can kick off a project with a lot of enthusiasm.

It’s necessary to stay focused on the heart of what you’re trying to accomplish in this phase: determine what the system is supposed to do. The most valuable tool for this is a collection of what are called “use-cases.” These are essentially descriptive answers to questions that start with “What does the system do if ...” For example, “What does the auto-teller do if a customer has just deposited a check within 24 hours and there’s not enough in the account without the check to provide the desired withdrawal?” The use-case then describes what the auto-teller does in that situation.

Use-case diagrams are intentionally very simple, to prevent you from getting bogged down in system implementation details prematurely:

Each stick person represents an “actor,” which is typically a human or some other kind of free agent (these can even be other computer systems). The box represents the boundary of your system. The ellipses represent the use cases themselves, which are units of functionality as they are perceived from outside of the system. That is, it doesn’t matter how the system is actually implemented, as long as it looks like this to the user.

A use-case does not need to be terribly complex, even if the underlying system is complex. It is only intended to show the system as it appears to the user. For example:

The use cases produce the requirements specifications, by determining all the interactions that the user may have with the system. You try to discover a full set of use-cases for your system, and once you’ve done that you have the core of what the system is supposed to do. The nice thing about focusing on use-cases is that they always bring you back to the essentials and keep you from drifting off into issues that aren’t critical for getting the job done. That is, if you have a full set of use-cases you can describe your system and move onto the next phase. You probably won’t get it all figured out perfectly at this phase, but that’s OK. Everything will reveal itself in the fullness of time, and if you demand a perfect system specification at this point you’ll get stuck.

If you get stuck, you can kick-start this phase by describing the system in a few paragraphs and then looking for nouns and verbs. The nouns become either actors or parts of use cases (or even entire use cases by themselves), and the verbs become the interactions between the two. You’ll be surprised at how useful a tool this can be; sometimes it will accomplish the lion’s share of the work for you.

Use-cases will identify key features in the system that will reveal some of the fundamental classes you’ll be using. For example, if you’re in the fireworks business, you may want to identify Workers, Firecrackers, and Customers; more specifically you’ll need Chemists, Assemblers, and Handlers; AmateurFirecrackers and ProfessionalFirecrackers; Buyers and Spectators. Even more specifically, you could identify YoungSpectators, OldSpectators, TeenageSpectators, and ParentSpectators.

Although it’s a black art, at this point some kind of scheduling can be quite useful. You now have an overview of what you’re building so you’ll probably be able to get some idea of how long it will take. A lot of factors come into play here: if you estimate a long schedule then the company might not decide to build it, or a manager might have already decided how long the project should take and will try to influence your estimate. But it’s best to have an honest schedule from the beginning and deal with the tough decisions early. There have been a lot of attempts to come up with accurate scheduling techniques (like techniques to predict the stock market), but probably the best approach is to rely on your experience and intuition. Get a gut feeling for how long it will really take, then double that and add 10 percent. Your gut feeling is probably correct; you can get something working in that time. The “doubling” will turn that into something decent, and the 10 percent will deal with final polishing and details [11]. However you want to explain it, and regardless of the moans and manipulations that happen when you reveal such a schedule, it just seems to work out that way.

Phase 2: How will we build it?

In this phase you must come up with a design that describes what the classes look like and how they will interact. An excellent tool in determining classes and interactions is the Class-Responsibility-Collaboration (CRC) card. Part of the value of this technique is that it’s so low-tech: you start out with a set of blank 3” by 5” cards, and you write on them. Each card represents a single class, and on the card you write:

  1. The name of the class. It’s important that this name capture the essence of what the class does, so that it makes sense at a glance.
  2. The “responsibilities” of the class: what it should do. This can typically be summarized by just stating the names of the member functions (since those names should be descriptive in a good design), but it does not preclude other notes. If you need to seed the process, look at the problem from a lazy programmer’s standpoint: What objects would you like to magically appear to solve your problem?
  3. The “collaborations” of the class: what other classes does it interact with? “Interact” is an intentionally broad term; it could mean aggregation or simply that some other object exists that will perform services for an object of the class. Collaborations should also consider the audience for this class. For example, if you create a class Firecracker, who is going to observe it, a Chemist or a Spectator? The former will want to know what chemicals go into the construction, and the latter will respond to the colors and shapes released when it explodes.
You may feel like the cards should be bigger because of all the information you’d like to get on them, but they are intentionally small, not only to keep your classes small but also to keep you from getting into too much detail too early. If you can’t fit all you need to know about a class on a small card, the class is too complex (either you’re getting too detailed, or you should create more than one class). The ideal class should be understood at a glance. The idea of CRC cards is to assist you in coming up with a first cut on the design, so that you can get the big picture and refine the design.

One of the great benefits of CRC cards is in communication. It’s best done real-time, in a group, without computers. Each person takes responsibility for several classes (which at first have no names or other information), and you run a live simulation by going through your use-cases and deciding what messages go to which objects to satisfy each use case. As you go through this process, you discover the classes you need along with their responsibilities and collaborations, and you fill out the cards as you do this. When you’ve moved through all the use cases, you should have a fairly complete first cut of your design.

Before I began using CRC cards, the most successful consulting experiences I had when coming up with an initial design involved standing in front of a team, who hadn’t built an OOP project before, and drawing objects on a whiteboard. We talked about how the objects should communicate with each other, and erased some of them and replaced them with other objects (effectively, I was managing all the “CRC cards” on the whiteboard). The team (who knew what the project was supposed to do) actually created the design; they “owned” the design rather than having it given to them. All I was doing was guiding the process by asking the right questions, trying out the assumptions and taking the feedback from the team to modify those assumptions. The true beauty of the process was that the team learned how to do object-oriented design not by reviewing abstract examples, but by working on the one design that was most interesting to them at that moment: theirs.

Once you’ve come up with a set of CRC cards, you may want to create a more formal description of your design using UML. There are a fair number of books on UML, and you can get the specification at http://www.rational.com. You don’t need to use UML, but it can be helpful, especially if you want to put a diagram up on the wall for everyone to ponder, which is a good idea. An alternative to UML is a textual description of the objects and their interfaces, but this can be limiting.

UML also provides a diagramming notation for describing the dynamic model of your system, for situations where the state transitions of a system or subsystem are dominant enough that they need their own diagrams (such as in a control system), and for describing the data structures, for systems or subsystems where data is a dominant factor (such as a database).

You’ll know you’re done with phase 2 when you have described the objects and their interfaces. Well, most of them – there are usually a few that slip through the cracks and don’t make themselves known until phase 3. But that’s OK. All you are concerned with is that you eventually discover all of your objects. It’s nice to discover them early in the process but OOP provides enough structure so that it’s not so bad if you discover them later. In fact, the design of an object tends to happen in five stages, throughout the process of program development.

Five stages of object design

The design life of an object is not limited to the period of time when you’re writing the program. Instead, the design of an object appears over a sequence of stages. It’s helpful to have this perspective because you stop expecting perfection right away; instead, you realize that the understanding of what an object does and what it should look like happens over time. This view also applies to the design of various types of programs; the pattern for a particular type of program emerges through struggling again and again with that problem (design patterns are covered in Chapter XX). Objects, too, have their patterns that emerge through understanding, use, and reuse.

1. Object discovery. This stage occurs during the initial analysis of a program. Objects may be discovered by looking for external factors and boundaries, duplication of elements in the system, and the smallest conceptual units. Some objects are obvious if you already have a set of class libraries. Commonality between classes suggesting base classes and inheritance may appear right away, or later in the design process.

2. Object assembly. As you’re building an object you’ll discover the need for new members that didn’t appear during discovery. The internal needs of the object may require new classes to support it.

3. System construction. Once again, more requirements for an object may appear at this later stage. As you learn, you evolve your objects. The need for communication and interconnection with other objects in the system may change the needs of your classes or require new classes. For example, here you may discover the need for facilitator or helper classes, such as a linked list, that contain little or no state information and simply help other classes to function.

4. System extension. As you add new features to a system you may discover that your previous design doesn’t support easy system extension. With this new information, you can restructure parts of the system, very possibly adding new classes or class hierarchies.

5. Object reuse. This is the real stress test for a class. If someone tries to reuse it in an entirely new situation, they’ll probably discover some shortcomings. As you change a class to adapt to more new programs, the general principles of the class will become clearer, until you have a truly reusable type.

Guidelines for object development

These stages suggest some guidelines when thinking about developing your classes:

  1. Let a specific problem generate a class, then let the class grow and mature during the solution of other problems.
  2. Remember, discovering the classes you need (and their interfaces) is the majority of the system design. If you already had those classes, this would be an easy project.
  3. Don’t force yourself to know everything at the beginning; learn as you go. That’s the way it will happen anyway.
  4. Start programming; get something working so you can prove or disprove your design. Don’t fear procedural-style spaghetti code – classes partition the problem and help control anarchy and entropy. Bad classes do not break good classes.
  5. Always keep it simple. Little clean objects with obvious utility are better than big complicated interfaces. When decision points come up, use a modified Occam’s Razor approach: Consider the choices and select the one that is simplest, because simple classes are almost always best. You can always start small and simple and expand the class interface when you understand it better, but as time goes on, it’s difficult to remove elements from a class.

Phase 3: Build it

This is the initial conversion from the rough design to a compiling body of code that can be tested, and especially that will prove or disprove your design. This is not a one-pass process, but rather the beginning of a series of writes and rewrites, as you’ll see in phase 4.

If you’re reading this book you’re probably a programmer, so now we’re at the part you’ve been trying to get to. By following a plan – no matter how simple and brief – and coming up with design structure before coding, you’ll discover that things fall together far more easily than if you dive in and start hacking, and you’ll also realize a great deal of satisfaction. Getting code to run and do what you want is fulfilling, and can easily become an obsession. But it’s my experience that coming up with an elegant solution is deeply satisfying at an entirely different level; it feels closer to art than technology. And elegance always pays off; it’s not a frivolous pursuit. Not only does it give you a program that’s easier to build and debug, but it’s also easier to understand and maintain, and that’s where the financial value lies.

After you build the system and get it running, it’s important to do a reality check, and here’s where the requirements analysis and system specification comes in. Go through your program and make sure that all the requirements are checked off, and that all the use-cases work the way they’re described (an even better approach is to use the requirements analysis and use-cases to generate test code). Now you’re done. Or are you?

Phase 4: Iteration

This is the point in the development cycle that has traditionally been called “maintenance,” a catch-all term that can mean everything from “getting it to work the way it was really supposed to in the first place” to “adding features that the customer forgot to mention” to the more traditional “fixing the bugs that show up” and “adding new features as the need arises.” So many misconceptions have been applied to the term “maintenance” that it has taken on a slightly deceiving quality, partly because it suggests that you’ve actually built a pristine program and all you need to do is change parts, oil it and keep it from rusting. Perhaps there’s a better term to describe what’s going on.

The term is iteration. That is, “You won’t get it right the first time, so give yourself the latitude to learn and to go back and make changes.” You might need to make a lot of changes as you learn and understand the problem more deeply. The elegance you’ll produce if you iterate until you get it right will pay off, both in the short and the long term. Iteration is where your program goes from good to great, and where those issues that you didn’t really understand in the first pass become clear. It’s also where your classes can evolve from single-project usage to reusable resources.

What it means to “get it right” isn’t just that the program works according to the requirements and the use-cases. It also means that the internal structure of the code makes sense to you, and feels like it fits together well, with no awkward syntax, oversized objects or ungainly exposed bits of code. In addition, you must have some sense that the program structure will survive the changes that it will inevitably go through during its lifetime, and that those changes can be made easily and cleanly. This is no small feat. You must not only understand what you’re building, but also how the program will evolve (what I call the vector of change ). Fortunately, object-oriented programming languages are particularly adept at supporting this kind of continuing modification – the boundaries created by the objects are what tend to keep the structure from breaking down. They are also what allow you to make changes – ones that would seem drastic in a procedural program – without causing earthquakes throughout your code. In fact, support for iteration might be the most important benefit of OOP.

With iteration, you create something that at least approximates what you think you’re building, and then you kick the tires, compare it to your requirements and see where it falls short. Then you can go back and fix it by redesigning and re-implementing the portions of the program that didn’t work right. [12] You might actually need to solve the problem, or an aspect of the problem, several times before you hit on the right solution. (A study of Design Patterns , described in Chapter XX, is usually helpful here.)

Iteration also occurs when you build a system, see that it matches your requirements and then discover it wasn’t actually what you wanted. When you see the system in operation, you find that you really wanted to solve a different problem. If you think this kind of iteration is going to happen, then you owe it to yourself to build your first version as quickly as possible so you can find out if it’s what you want.

Iteration is closely tied to incremental development . Incremental development means that you start with the core of your system and implement it as a framework upon which to build the rest of the system piece by piece. Then you start adding features one at a time. The trick to this is in designing a framework that will accommodate all the features you plan to add to it. (See Chapter XX for more insight into this issue.) The advantage is that once you get the core framework working, each feature you add is like a small project in itself rather than part of a big project. Also, new features that are incorporated later in the development or maintenance phases can be added more easily. OOP supports incremental development because if your program is designed well, your increments will turn out to be discrete objects or groups of objects.

Perhaps the most important thing to remember is that by default – by definition, really – if you modify a class its super- and subclasses will still function. You need not fear modification; it won’t necessarily break the program, and any change in the outcome will be limited to subclasses and/or specific collaborators of the class you change.

You have to know when to stop iterating the design. Ideally, you achieve target functionality and are in the process of refinement and addition of new features when the deadline comes along and forces you to stop and ship that version. (Remember, software is a subscription business.)

Plans pay off

Of course you wouldn’t build a house without a lot of carefully-drawn plans. If you build a deck or a dog house, your plans won’t be so elaborate but you’ll still probably start with some kind of sketches to guide you on your way. Software development has gone to extremes. For a long time, people didn’t have much structure in their development, but then big projects began failing. In reaction, we ended up with methodologies that had an intimidating amount of structure and detail, primarily intended for those big projects. These methodologies were too scary to use – it looked like you’d spend all your time writing documents and no time programming. (This was often the case.) I hope that what I’ve shown you here suggests a middle path – a sliding scale. Use an approach that fits your needs (and your personality). No matter how minimal you choose to make it, some kind of plan will make a big improvement in your project as opposed to no plan at all. Remember that, by most estimates, over 50 percent of projects fail (some estimates go up to 70 percent!).


[10] My rule of thumb for estimating such projects: If there’s more than one wild card, don’t even try to plan how long it’s going to take or how much it will cost. There are too many degrees of freedom.

[11] My personal take on this has changed lately. Doubling and adding 10 percent will give you a reasonably accurate estimate (assuming there are not too many wild-card factors), but you still have to work quite dilligently to finish in that time. If you actually want time to really make it elegant and to enjoy yourself in the process, the correct multiplier is more like three or four times, I believe.

[12] This is something like “rapid prototyping,” where you were supposed to build a quick-and-dirty version so that you could learn about the system, and then throw away your prototype and build it right. The trouble with rapid prototyping is that people didn’t throw away the prototype, but instead built upon it. Combined with the lack of structure in procedural programming, this often leads to messy systems that are expensive to maintain.

Contents | Prev | Next


Contact: webmaster@codeguru.com
CodeGuru - the website for developers.
[an error occurred while processing this directive]