Выбрать главу

Since post-conditions are totally your responsibility, you might think assertions also apply, and you would be partially right. It is appropriate to use an assertion for any invariant at any time, including when a function has finished its work. This especially applies to class member functions that maintain the state of an object. In the MyVector example earlier, for instance, a reasonable invariant for all public member functions would be

assert(0 <= nextSlot && nextSlot <= capacity);

or, if nextSlot is an unsigned integer, simply

assert(nextSlot <= capacity);

Such an invariant is called a class invariant and can reasonably be enforced by an assertion. Subclasses play the role of subcontractor to their base classes in that they must maintain the original contract the base class has with its clients. For this reason, the pre-conditions in derived classes must impose no extra requirements beyond those in the base contract, and the post-conditions must deliver at least as much.[17] 

Validating results returned to the client, however, is nothing more or less than testing, so using post-condition assertions in this case would be duplicating work. There’s nothing wrong with it; it’s just an exercise in redundancy. Yes, it’s good documentation, but more than one developer has been fooled into using post-condition assertions as a substitute for unit testing. Bad idea!.

A simple unit test framework

Writing software is all about meeting requirements.[18] It doesn’t take much experience, however, to figure out that coming up with requirements in the first place is no easy task, and, more important, requirements are not static. It’s not unheard of to discover at a weekly project meeting that what you just spent the week doing is not exactly what the users really want.

Frustrating? Yes. Reasonable? Also, yes! It is unreasonable to expect mere humans to be able to articulate software requirements in detail without sampling an evolving, working system. It's much better to specify a little, design a little, code a little, test a little. Then, after evaluating the outcome, do it all over again. The ability to develop from soup to nuts in such an iterative fashion is one of the great advances of this object-oriented era in software history. It requires nimble programmers who can craft resilient code. Change is hard.

Ironically, another impetus for change comes from you, the programmer. The craftsperson in you likely has the habit of continually improving the physical design of working code. What maintenance programmer hasn’t had occasion to curse the aging, flagship company product as a convoluted patchwork of spaghetti, wholly resistant to modification? Management’s knee-jerk reluctance to let you tamper with a functioning system, while not totally unfounded, robs code of the resilience it needs to endure. "If it’s not broken, don’t fix it" eventually gives way to, "We can’t fix it—rewrite it." Change is necessary.

Fortunately, our industry has finally gotten used to the discipline of refactoring, the art of internally restructuring code to improve its design, without changing the functionality visible to the user.[19] Such tweaks include extracting a new function from another, or its inverse, combining member functions; replacing a member function with an object; parameterizing a member function or class; and replacing conditionals with polymorphism. Refactoring helps code embrace evolution.

Whether the force for change comes from users or programmers, however, there is still the risk that changes today will break what worked yesterday. What is needed is a way to build code that withstands the winds of change and actually improves over time.

Many practices purport to support such a quick-on-your-feet motif, of which Extreme Programming is only one.[20] In this section we explore what we think is the key to making flexible, incremental development succeed: a ridiculously easy-to-use automated unit test framework. (Please note that we in no way mean to de-emphasize the role of testers, software professionals who test others’ code for a living. They are indispensable. We are merely describing a way to help developers write better code.).

Developers write unit tests to gain the confidence to say the two most important things that any developer can say:

1.I understand the requirements.

2.My code meets those requirements to the best of my knowledge.

There is no better way to ensure that you know what the code you're about to write should do than to write the unit tests first. This simple exercise helps focus the mind on the task ahead and will likely lead to working code faster than just jumping into coding. Or, to express it in XP terms: Testing + Programming is faster than just Programming. Writing tests first also puts you on guard up front against boundary conditions that might cause your code to break, so your code is more robust right out of the chute.

Once your code passes all your tests, you have the peace of mind that if the system you contribute to isn't working, it's not your fault. The statement "All my tests pass" is a powerful trump card in the workplace that cuts through any amount of politics and hand waving.

Automated testing

So what does a unit test look like? Too often developers just use some well-behaved input to produce some expected output, which they inspect visually. Two dangers exist in this approach. First, programs don't always receive only well-behaved input. We all know that we should test the boundaries of program input, but it's hard to think about this when you're trying to just get things working. If you write the test for a function first before you start coding, you can wear your "tester hat" and ask yourself, "What could possibly make this break?" Code a test that will prove the function you'll write isn't broken, and then put on your developer hat and make it happen. You'll write better code than if you hadn't written the test first.

The second danger is that inspecting output visually is tedious and error prone. Most any such thing a human can do a computer can do, but without human error. It's better to formulate tests as collections of Boolean expressions and have a test program report any failures.

For example, suppose you need to build a Date class that has the following properties:

·         A date can be initialized with a string (YYYYMMDD), three integers (Y, M, D), or nothing (giving today's date).

·         A date object can yield its year, month, and day or a string of the form "YYYYMMDD".

·         All relational comparisons are available, as well as computing the duration between two dates (in years, months, and days).

·         Dates to be compared need to be able to span an arbitrary number of centuries (for example, 1600–2200).

Your class can store three integers representing the year, month, and day. (Just be sure the year is at least 16 bits in size to satisfy the last bulleted item.) The interface for your Date class might look like this:.

вернуться

17

There is a nice phrase to help remember this phenomenon: “Require no more; promise no less,” first coined in C++ FAQs, by Marshall Cline and Greg Lomow (Addison-Wesley, 1994). Since pre-conditions can weaken in derived classes, we say that they are contravariant, and, conversely, post-conditions are covariant (which explains why we mentioned the covariance of exception specifications in Chapter 1).

вернуться

18

This section is based on Chuck’s article, “The Simplest Automated Unit Test Framework That Could Possibly Work”, C/C++ Users Journal, Sept. 2000.

вернуться

19

A good book on this subject is Martin Fowler's Refactoring: Improving the Design of Existing Code (Addison-Wesley, 2000). See also www.refactoring.com. Refactoring is a crucial practice of Extreme Programming (XP).

вернуться

20

Lightweight methodologies such as XP have “joined forces” in the Agile Alliance (see http://www.agilealliance.org/home).