Скачать книгу

example, suppose you’re building a system to manage the results of ostrich races. You might decide the project needs the following major pieces:

      ● Database (to hold the data)

      ● Classes (for example, Race, Ostrich, and Jockey classes)

      ● User interfaces (to enter Ostrich and Jockey data, enter race results, produce result reports, and create new races)

      ● External interfaces (to send information and spam to participants and fans via e-mail, text message, voice mail, and anything else we can think of)

      You should make sure that the high-level design covers every aspect of the requirements. It should specify what the pieces do and how they should interact, but it should include as few details as possible about how the pieces do their jobs.

      TO DESIGN OR NOT TO DESIGN, THAT IS THE QUESTION

      At this point, fans of extreme programming, Scrum, and other incremental development approaches may be rolling their eyes, snorting in derision and muttering about how those methodologies don’t need high-level designs.

      Let’s defer this argument until Chapter 5, “High-Level Design,” which talks about high-level design in greater detail. For now, I’ll just claim that every design methodology needs design, even if it doesn’t come in the form of a giant written design specification carved into a block of marble.

      LOW-LEVEL DESIGN

      After your high-level design breaks the project into pieces, you can assign those pieces to groups within the project so that they can work on low-level designs. The low-level design includes information about how that piece of the project should work. The design doesn’t need to give every last nitpicky detail necessary to implement the project’s major pieces, but they should give enough guidance to the developers who will implement those pieces.

      For example, the ostrich racing application’s database piece would include an initial design for the database. It should sketch out the tables that will hold the race, ostrich, and jockey information.

      At this point you will also discover interactions between the different pieces of the project that may require changes here and there. The ostrich project’s external interfaces might require a new table to hold e-mail, text messaging, and other information for fans.

      DEVELOPMENT

      After you’ve created the high- and low-level designs, it’s time for the programmers to get to work. (Actually, the programmers should have been hard at work gathering requirements, creating the high-level designs, and refining them into low-level designs, but development is the part that most programmers enjoy the most.) The programmers continue refining the low-level designs until they know how to implement those designs in code.

      (In fact, in one of my favorite development techniques, you basically just keep refining the design to give more and more detail until it would be easier to just write the code instead. Then you do exactly that.)

      As the programmers write the code, they test it to make sure it doesn’t contain any bugs.

      At this point, any experienced developers should be snickering if not actually laughing out loud. It’s a programming axiom that no nontrivial program is completely bug-free. So let me rephrase the previous paragraph.

      As the programmers write the code, they test it to find and remove as many bugs as they reasonably can.

      TESTING

      Effectively testing your own code is extremely hard. If you just wrote the code, you obviously didn’t insert bugs intentionally. If you knew there was a bug in the code, you would have fixed it before you wrote it. That idea often leads programmers to assume their code is correct (I guess they’re just naturally optimistic) so they don’t always test it as thoroughly as they should.

      Even if a particular piece of code is thoroughly tested and contains no (or few) bugs, there’s no guarantee that it will work properly with the other parts of the system.

      One way to address both of these problems (developers don’t test their own code well and the pieces may not work together) is to perform different kinds of tests. First developers test their own code. Then testers who didn’t write the code test it. After a piece of code seems to work properly, it is integrated into the rest of the project, and the whole thing is tested to see if the new code broke anything.

      Any time a test fails, the programmers dive back into the code to figure out what’s going wrong and how to fix it. After any repairs, the code goes back into the queue for retesting.

      A SWARM OF BUGS

      At this point you may wonder why you need to retest the code. After all, you just fixed it, right?

      Unfortunately fixing a bug often creates a new bug. Sometimes the bug fix is incorrect. Other times it breaks another piece of code that depended on the original buggy behavior. In the known bug hides an unknown bug.

      Still other times the programmer might change some correct behavior to a different correct behavior without realizing that some other code depended on the original correct behavior. (Imagine if someone switched the arrangement of your hot and cold water faucets. Either arrangement would work just fine, but you may get a nasty surprise the next time you take a shower.)

      Any time you change the code, whether by adding new code or fixing old code, you need to test it to make sure everything works as it should.

      Unfortunately, you can never be certain that you’ve caught every bug. If you run your tests and don’t find anything wrong, that doesn’t mean there are no bugs, just that you haven’t found them. As programming pioneer Edsger W. Dijkstra said, “Testing shows the presence, not the absence of bugs.” (This issue can become philosophical. If a bug is undetected, is it still a bug?)

      The best you can do is test and fix bugs until they occur at an acceptably low rate. If bugs don’t bother users too frequently or too severely when they do occur, then you’re ready to move on to deployment.

       EXAMPLE Counting Bugs

      Suppose requirements gathering, high-level design, low-level design, and development works like this: Every time you make a decision, the next task in the sequence includes two more decisions that depend on the first one. For example, when you make a requirements decision, the high-level design includes two decisions that depend on it. (This isn’t exactly the way it works, but it’s not as ridiculous as you might wish.)

      Now suppose you made a mistake during requirements gathering. (The customer said the application had to support 30 users with a 5-second response time, but you heard 5 users with a 30-second response time.)

      If you detect the error during the requirements gathering phase, you need to fix only that one error. But how many incorrect decisions could depend on that one mistake if you don’t discover the problem until after development is complete?

      The one mistake in requirements gathering leads to two decisions in high-level design that could be incorrect.

      Each of the two possible mistakes in high-level design leads to two new decisions in low-level design that could also be wrong, giving a total of 2 × 2 = 4 possible mistakes in low-level design.

      Each of the four suspicious low-level design decisions lead to two more decisions during development, giving a total of 4 × 2 = 8 possible mistakes during development.

Adding up all the mistakes in requirements gathering, high-level design, low-level design, and development gives a total of 1 + 2 + 4 + 8 = 15 possible mistakes. Figure 1.1 shows how the potential mistakes propagate.

Figure 1.1 The circles represent possible mistakes at different stages of development. One early mistake can lead to lots of later mistakes.

      In this example, you have 15 times as many decisions to track down, examine, and possibly fix than you would have if you had

Скачать книгу