7. What Is Agile Design?

image

© Jennifer M. Kohnke

After reviewing the software development life cycle as I understood it, I concluded that the only software documentation that actually seems to satisfy the criteria of an engineering design is the source code listings.

—Jack Reeves

In 1992, Jack Reeves wrote a seminal article—“What Is Software Design?”—in the C++ Journal.1 In this article, Reeves argued that the design of a software system is documented primarily by its source code, that diagrams representing the source code are ancillary to the design and are not the design itself. As it turns out, Jack’s article was a harbinger of agile development.

In the pages that follow, we often talk about “the design.” You should not take that to mean a set of UML diagrams separate from the code. A set of UML diagrams may represent parts of a design, but those diagrams are not the design. The design of a software project is an abstract concept. It has to do with the overall shape and structure of the program, as well as the detailed shape and structure of each module, class, and method. The design can be represented by many different media, but its final embodiment is source code. In the end, the source code is the design.

Design Smells

If you are lucky, you start a project with a clear picture of what you want the system to be. The design of the system is a vital image in your mind. If you are luckier still, the clarity of that design makes it to the first release.

But then something goes wrong. The software starts to rot like a piece of bad meat. As time goes by, the rotting continues. Ugly, festering sores and boils accumulate in the code, making it more and more difficult to maintain. Eventually, the sheer effort required to make even the simplest of changes becomes so onerous that the developers and front-line managers cry for a redesign.

Such redesigns rarely succeed. Although the designers start out with good intentions, they find that they are shooting at a moving target. The old system continues to evolve and change, and the new design must keep up. The warts and ulcers accumulate in the new design before it ever makes it to its first release.

Design Smells—The Odors of Rotting Software

You know that the software is rotting when it starts to exhibit any of the following odors.

• Rigidity

• Fragility

• Immobility

• Viscosity

• Needless complexity

• Needless repetition

• Opacity

Rigidity

Rigidity is the tendency for software to be difficult to change, even in simple ways. A design is rigid if a single change causes a cascade of subsequent changes in dependent modules. The more modules that must be changed, the more rigid the design.

Most developers have faced this situation in one way or another. They are asked to make what appears to be a simple change. They look the change over and make a reasonable estimate of the work required. But later, as they work through the change, they find that there are unanticipated repercussions to the change. The developers find themselves chasing the change through huge portions of the code, modifying far more modules than they had first estimated, and discovering thread after thread of other changes that they must remember to make. In the end, the changes take far longer than the initial estimate. When asked why their estimate was so poor, they repeat the traditional software developers lament: “It was a lot more complicated than I thought!”

Fragility

Fragility is the tendency of a program to break in many places when a single change is made. Often, the new problems are in areas that have no conceptual relationship with the area that was changed. Fixing those problems leads to even more problems, and the development team begins to resemble a dog chasing its tail.

As the fragility of a module increases, the likelihood that a change will introduce unexpected problems approaches certainty. This seems absurd, but such modules are not at all uncommon. These are the modules that are continually in need of repair, the ones that are never off the bug list. These modules are the ones that the developers know need to be redesigned, but nobody wants to face the spectre of redesigning them. These modules are the ones that get worse the more you fix them.

Immobility

A design is immobile when it contains parts that could be useful in other systems, but the effort and risk involved with separating those parts from the original system are too great. This is an unfortunate but very common occurrence.

Viscosity

Viscosity comes in two forms: viscosity of the software and viscosity of the environment. When faced with a change, developers usually find more than one way to make that change. Some of the ways preserve the design; others do not (i.e., they are hacks). When the design-preserving methods are more difficult to use than the hacks, the viscosity of the design is high. It is easy to do the wrong thing but difficult to do the right thing. We want to design our software such that the changes that preserve the design are easy to make.

Viscosity of environment comes about when the development environment is slow and inefficient. For example, if compile times are very long, developers will be tempted to make changes that don’t force large recompiles, even though those changes don’t preserve the design. If the source code control system requires hours to check in just a few files, developers will be tempted to make changes that require as few check-ins as possible, regardless of whether the design is preserved.

In both cases, a viscous project is one in which the design of the software is difficult to preserve. We want to create systems and project environments that make it easy to preserve and improve the design.

Needless Complexity

A design smells of needless complexity when it contains elements that aren’t currently useful. This frequently happens when developers anticipate changes to the requirements and put facilities in the software to deal with those potential changes. At first, this may seem like a good thing to do. After all, preparing for future changes should keep our code flexible and prevent nightmarish changes later.

Unfortunately, the effect is often just the opposite. By preparing for many contingencies, the design becomes littered with constructs that are never used. Some of those preparations may pay off, but many more do not. Meanwhile, the design carries the weight of these unused design elements. This makes the software complex and difficult to understand.

Needless Repetition

Cut and paste may be useful text-editing operations, but they can be disastrous code-editing operations. All too often, software systems are built on dozens or hundreds of repeated code elements. It happens like this: Ralph needs to write some code that fravles the arvadent.2 He looks around in other parts of the code where he suspects other arvadent fravling has occurred and finds a suitable stretch of code. He cuts and pastes that code into his module and makes the suitable modifications.

Unbeknownst to Ralph, the code he scraped up with his mouse was put there by Todd, who scraped it out of a module written by Lilly. Lilly was the first to fravle an arvadent, but she realized that fravling an arvadent was very similar to fravling a garnatosh. She found some code somewhere that fravled a garnatosh, cut and paste it into her module, and modified it as necessary.

When the same code appears over and over again, in slightly different forms, the developers are missing an abstraction. Finding all the repetition and eliminating it with an appropriate abstraction may not be high on their priority list, but it would go a long way toward making the system easier to understand and maintain.

When there is redundant code in the system, the job of changing the system can become arduous. Bugs found in such a repeating unit have to be fixed in every repetition. However, since each repetition is slightly different from every other, the fix is not always the same.

Opacity

Opacity is the tendency of a module to be difficult to understand. Code can be written in a clear and expressive manner, or it can be written in an opaque and convoluted manner. Code that evolves over time tends to become more and more opaque with age. A constant effort to keep the code clear and expressive is required in order to keep opacity to a minimum.

When developers first write a module, the code may seem clear to them. After all, they have immersed themselves in it and understand it at an intimate level. Later, after the intimacy has worn off, they may return to that module and wonder how they could have written anything so awful. To prevent this, developers need to put themselves in their readers’ shoes and make a concerted effort to refactor their code so that their readers can understand it. They also need to have their code reviewed by others.

Why Software Rots

In nonagile environments, designs degrade because requirements change in ways that the initial design did not anticipate. Often, these changes need to be made quickly and may be made by developers who are not familiar with the original design philosophy. So, though the change to the design works, it somehow violates the original design. Bit by bit, as the changes continue, these violations accumulate until malignancy sets in.

However, we cannot blame the drifting of the requirements for the degradation of the design. We, as software developers, know full well that requirements change. Indeed, most of us realize that the requirements are the most volatile elements in the project. If our designs are failing owing to the constant rain of changing requirements, it is our designs and practices that are at fault. We must somehow find a way to make our designs resilient to such changes and use practices that protect them from rotting.

An agile team thrives on change. The team invests little up front and so is not vested in an aging initial design. Rather, the team keeps the design of the system as clean and simple as possible and backs it up with lots of unit tests and acceptance tests. This keeps the design flexible and easy to change. The team takes advantage of that flexibility in order to continuously improve the design; thus, each iteration ends with a system whose design is as appropriate as it can be for the requirements in that iteration.

The Copy Program

A Familiar Scenario

Watching a design rot may help illustrate the preceding points. Let’s say that your boss comes to you early Monday morning and asks you to write a program that copies characters from the keyboard to the printer. Doing some quick mental exercises in your head, you conclude that this will be less than ten lines of code. Design and coding time should be a lot less than 1 hour. What with cross-functional group meetings, quality education meetings, daily group progress meetings, and the three current crises in the field, this program ought to take you about a week to complete—if you stay after hours. However, you always multiply your estimates by 3.

“Three weeks,” you tell your boss. He harumphs and walks away, leaving you to your task.

The initial design

You have a bit of time right now before that process review meeting begins, so you decide to map out a design for the program. Using structured design, you come up with the structure chart in Figure 7-1.

Figure 7-1. Copy program structure chart

image

There are three modules, or subprograms, in the application. The Copy module calls the other two. The Copy program fetches characters from the Read Keyboard module and routes them to the Write Printer module.

You look at your design and see that it is good. You smile and then leave your office to go to that review. At least you’ll be able to get a little sleep there.

On Tuesday, you come in a bit early so that you can finish up the Copy program. Unfortunately, one of the crises in the field has warmed up overnight, and you have to go to the lab and help debug a problem. On your lunch break, which you finally take at 3 PM, you manage to type in the code for the Copy program. The result is Listing 7-1.

You just manage to save the edit when you realize that you are already late for a quality meeting. You know that this is an important one; they are going to be talking about the magnitude of zero defects. So you wolf down your Twinkies and Coke and head off to the meeting.


Listing 7-1. The Copy Program

public class Copier
{
  public static void Copy()
  {
    int c;
    while((c=Keyboard.Read()) != -1)
      Printer.Write(c);
  }
}


On Wednesday, you come in early again, and this time nothing seems to be amiss. So you pull up the source code for the Copy program and begin to compile it. Lo and behold, it compiles first time with no errors! It’s a good thing, too, because your boss calls you into an unscheduled meeting about the need to conserve laser printer toner.

On Thursday, after spending 4 hours on the phone walking a service technician in Rocky Mount, North Carolina, through the remote debugging and error-logging commands in one of the more obscure components of the system, you grab a Hoho and then test your Copy program. It works, first time! Good thing, too. Because your new co-op student has just erased the master source code directory from the server, and you have to go find the latest backup tapes and restore it. Of course, the last full backup was taken 3 months ago, and you have 94 incremental backups to restore on top of it.

Friday is completely unbooked. Good thing, too, because it takes all day to get the Copy program successfully loaded into your source code control system.

Of course, the program is a raging success and gets deployed throughout your company. Your reputation as an ace programmer is once again confirmed, and you bask in the glory of your achievements. With luck, you might actually produce 30 lines of code this year!

The requirements they are a’changin’

A few months later, your boss comes to you and says that the Copy program should also be able to read from the paper tape reader. You gnash your teeth and roll your eyes. You wonder why people are always changing the requirements. Your program wasn’t designed for a paper tape reader! You warn your boss that changes like this are going to destroy the elegance of your design. Nevertheless, your boss is adamant, saying that the users really need to read characters from the paper tape reader from time to time.

So you sigh and plan your modifications. You’d like to add a Boolean argument to the Copy function. If true, you’d read from the paper tape reader; if false, you’d read from the keyboard as before. Unfortunately, so many other programs use the Copy program now that you can’t change the interface. Changing the interface would cause weeks and weeks of recompiling and retesting. The system test engineers alone would lynch you, not to mention the seven people in the configuration control group. And the process police would have a field day, forcing all kinds of code reviews for every module that called Copy!

No, changing the interface is out. But then how can you let the Copy program know that it must read from the paper tape reader? Of course! You’ll use a global! You’ll also use the best and most useful feature of the C family of languages, the ?: operator! Listing 7-2 shows the result.


Listing 7-2. First modification of Copy program

public class Copier
{
  //remember to reset this flag
  public static bool ptFlag = false;
  public static void Copy()
  {
    int c;
    while((c=(ptFlag ? PaperTape.Read()
                      : Keyboard.Read())) != -1)
      Printer.Write(c);
  }
}


Copy callers who want to read from the paper tape reader must first set the ptFlag to true. Then they can call Copy, and it will happily read from the paper tape reader. Once Copy returns, the caller must reset the ptFlag; otherwise, the next caller may mistakenly read from the paper tape reader rather than from the keyboard. To remind the programmers of their duty to reset this flag, you have added an appropriate comment.

Once again, you release your software to critical acclaim. It is even more successful than before, and hordes of eager programmers are waiting for an opportunity to use it. Life is good.

Give ‘em an inch

Some weeks later, your boss—who is still your boss despite three corporatewide reorganizations in as many months—tells you that the customers would sometimes like the Copy program to output to the paper tape punch. Customers! They are always ruining your designs. Writing software would be a lot easier if it weren’t for customers. You tell your boss that these incessant changes are having a profound negative effect on the elegance of your design, warning that if changes continue at this horrid pace, the software will be impossible to maintain before year’s end. Your boss nods knowingly and then tells you to make the change anyway.

This design change is similar to the one before it. All we need is another global and another ?: operator! Listing 7-3 shows the result of your endeavors.

You are especially proud of the fact that you remembered to change the comment. Still, you worry that the structure of your program is beginning to topple. Any more changes to the input device will certainly force you to completely restructure the while loop conditional. Perhaps it’s time to dust off your resume.


Listing 7-3. Second modification of Copy program

public class Copier
{
  //remember to reset these flags
  public static bool ptFlag = false;
  public static bool punchFlag = false;
  public static void Copy()
  {
    int c;
    while((c=(ptFlag ? PaperTape.Read()
                      : Keyboard.Read())) != -1)
      punchFlag ? PaperTape.Punch(c) : Printer.Write(c);
  }
}


Expect changes

I’ll leave it to you to determine just how much of the preceding was satirical exaggeration. The point of the story is to show how the design of a program can rapidly degrade in the presence of change. The original design of the Copy program was simple and elegant. Yet after only two changes, it has begun to show the signs of rigidity, fragility, immobility, complexity, redundancy, and opacity. This trend is certainly going to continue, and the program will become a mess.

We might sit back and blame this on the changes. We might complain that the program was well designed for the original spec and that the subsequent changes to the spec caused the design to degrade. However, this ignores one of the most prominent facts in software development: Requirements always change!

Remember, the most volatile things in most software projects are the requirements. The requirements are continuously in a state of flux. This is a fact that we, as developers, must accept! We live in a world of changing requirements, and our job is to make sure that our software can survive those changes. If the design of our software degrades because the requirements have changed, we are not being agile.

Agile Design of the Copy Program

An agile development team might begin exactly the same way, with the code in Listing 7-1.3 When the boss asked to make the program read from the paper tape reader, the developers would have responded by changing the design to be resilient to that kind of change. The result might have been something like Listing 7-4.


Listing 7-4. Agile version 2 of Copy

public interface Reader
{
  int Read();
}

public class KeyboardReader : Reader
{
  public int Read() {return Keyboard.Read();}
}

public class Copier
{
  public static Reader reader = new KeyboardReader();
  public static void Copy()
  {
    int c;
    while((c=(reader.Read())) != -1)
      Printer.Write(c);
  }
}


Instead of trying to patch the design to make the new requirement work, the team seizes the opportunity to improve the design so that it will be resilient to that kind of change in the future. From now on, whenever the boss asks for a new kind of input device, the team will be able to respond in a way that does not cause degradation to the Copy program.

The team has followed the Open/Closed Principle (OCP), which we describe in Chapter 9. This principle directs us to design our modules so that they can be extended without modification. That’s exactly what the team has done. Every new input device that the boss asks for can be provided without modifying the Copy program.

Note, however, that when it first designed the module, the team did not try to anticipate how the program was going to change. Instead, the team wrote the module in the simplest way possible. It was only when the requirements did eventually change that the team changed the design of the module to be resilient to that kind of change.

One could argue that the team did only half the job. While the developers were protecting themselves from different input devices, they could also have protected themselves from different output devices. However, the team really has no idea whether the output devices will ever change. To add the extra protection now would be work that served no current puprose. It’s clear that if such protection is needed it will be easy to add later. So, there’s really no reason to add it now.

Following agile practices

The agile developers in our example built an abstract class to protect them from changes to the input device. How did they know how to do that? The answer lies with one of the fundamental tenets of object-oriented design.

The initial design of the Copy program is inflexible because of the direction of its dependencies. Look again at Figure 7-1. Note that the Copy module depends directly on the KeyboardReader and the PrinterWriter. The Copy module is a high-level module in this application. It sets the policy of the application. It knows how to copy characters. Unfortunately, it has also been made dependent on the low-level details of the keyboard and the printer. Thus, when the low-level details change, the high-level policy is affected.

Once the inflexibility was exposed, the agile developers knew that the dependency from the Copy module to the input device needed to be inverted, using the Dependency Inversion Principle (DIP) in Chapter 11, so that Copy would no longer depend on the input device. They then used the STRATEGY pattern, discussed in Chapter 22, to create the desired inversion.

So, in short, the agile developers knew what to do because they followed these steps.

  1. They detected the problem by following agile practices.
  2. They diagnosed the problem by applying design principles.
  3. They solved the problem by applying an appropriate design pattern.

The interplay between these three aspects of software development is the act of design.

Keeping the design as good as it can be

Agile developers are dedicated to keeping the design as appropriate and clean as possible. This is not a haphazard or tentative commitment. Agile developers do not “clean up” the design every few weeks. Rather, they keep the software as clean, simple, and expressive as they possibly can—every day, every hour, and every minute. They never say, “We’ll go back and fix that later.” They never let the rot begin.

The attitude that agile developers have toward the design of the software is the same attitude that surgeons have toward sterile procedure. Sterile procedure is what makes surgery possible. Without it, the risk of infection would be far too high to tolerate. Agile developers feel the same way about their designs. The risk of letting even the tiniest bit of rot begin is too high to tolerate.

The design must remain clean. And since the source code is the most important expression of the design, it too must remain clean. Professionalism dicates that we, as software developers, cannot tolerate code rot.

Conclusion

So, what is agile design? Agile design is a process, not an event. It’s the continous application of principles, patterns, and practices to improve the structure and readability of the software. It is the dedication to keep the design of the system as simple, clean, and expressive as possible at all times.

In the chapters that follow, we’ll be investigating the principles and patterns of software design. As you read, remember that an agile developer does not apply those principles and patterns to a big, up-front design. Rather, they are applied from iteration to iteration in an attempt to keep the code, and the design it embodies, clean.

Bibliography

[Reeves92] Jack Reeves, “What Is Software Design?,” C++ Journal, (2), 1992. Also available at www.bleading-edge.com/Publications/C++Journal/Cpjour2.htm.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.218.55.14