Chapter 6. Specification of Observable Behavior

FAQ 6.01 What is the purpose of this chapter?

image

To provide a practical technique that reduces the ripple effect during both development and maintenance. The technique provided in this chapter is fundamental to understanding proper inheritance as well as reducing both short-term and long-term costs, especially for large systems.

The basic idea is to unambiguously state the external behavior of each member function in some well-known location, then for other programmers to rely on this specification rather than digging into the code and relying on the implementation. This technique is sometimes called programming by contract, and it is extremely valuable whenever a software system is large enough that most programmers can't remember all the ifs, ands and buts of every member function of every class.

FAQ 6.02 Should users of a member function rely on what the code actually does or on the specification?

image

The specification!

A member function's specification unambiguously defines its externally observable behavior across all possible implementations. The specification of a member function is more than simply the member function's signature. The specification captures the essential service that the member function provides to its users in an implementation-independent fashion. This is an essential part of abstraction and encapsulation. If a class's developers do not provide a full and complete specification, then they have failed as OO programmers because they have failed to properly separate the interface from the implementation and they have forced users to look at the implementation.

Many software organizations systematically fail to observe this critical guideline. Developers in these organizations are trained to rely on what the code does instead of what it promises to do. Users of a member function must be able to rely only on the member function's specification, not on the implementation of the specification. In fact, there are only a few times when users legitimately need to look at the source code to find out what a member function actually does (such as when you are inspecting the code to ensure it fulfills its promise).

In the following example, suppose Version1 and Version2 represent two versions of the same class. Note that the specification (that is, the description of the behavior that other programmers are supposed to rely on) of member function f() did not change even though the implementation of Version2::f() is different from that of Version1::f().

In this case, the specification says that the member function f() “Promises that the return value will be some even number,” and presumably this captures the essential behavior of f(). The designer may have done this to allow some implementations to return 4, while other implementations return a random even number between 0 and 100, while other implementations return even numbers less than zero. Users who rely only on this specification won't be hurt by the new version since their code would work for any even number. On the other hand, users who rely on what the code actually does (i.e., who rely on member function f() always returning 4) could break since they may rely on the value being greater than zero or greater than 2 or one of a hundred other assumptions.

image

Never assume that a member function will always do what the code currently does. The code will change, and the specification is generally more stable than the code.

FAQ 6.03 What are the advantages of relying on the specification rather than the implementation?

Time, completeness, flexibility, fixability, extensibility, understandability, and scalability.

Time: It is far easier to read the member function's specification than to reverse-engineer its actual behavior.

Completeness: If a specification is insufficient or absent, the class is broken and must be fixed. By forcing users to rely on the specification rather than the implementation, users report an insufficient specification as a serious error. Thus specifications are developed early in the development life cycle and will be maintained throughout the life cycle

Flexibility: The code of a member function may (and generally does) change. When it does, user code that relies only on the specification doesn't break, assuming the new behavior is compatible with the old specification. However, user code that depends on how the member function was implemented may break when legitimate modifications are made.

Fixability: A defect can be defined as a member function that doesn't fulfill its specification. The right course of action when specifications have been written is usually to make the member function do what it is supposed to do, rather than changing the specification to reflect the erroneous implementation. However, it is unclear how to fix the defect in organizations where specifications are not used and developers have to rely on what the code does instead of what it promises to do. When the code is rewritten to fix the defect, other defects will undoubtedly appear in other portions of the system that relied on the earlier version of code. This ripple effect results in the development team “chasing” the defect through the system as they have to make more and more modifications to the system to try and make it work.

Extensibility: Adaptable specifications give latitude to derived classes. If users rely on the code of the base class, users may break when supplied with a derived class.

Understandability: By providing accurate specifications, the system's behavior can be more easily understood. In systems that don't use complete and consistent specifications, the long-term result is an overreliance on those rare individuals, the system experts, who can understand the entire system at once. For example, to repair a certain defect, either part X or part Y must be modified. Without specification of what these parts are supposed to do, only the system expert can determine whether any other component is going to break if X or Y are changed. Specifications enable average developers to make more of these decisions. From a business perspective, this mitigates risk if the system expert is run over by a truck.

Scalability: The larger the system the more important it is to separate the specification from the implementation.

Write code to fulfill a specification, not the other way around.

FAQ 6.04 What are advertised requirements and advertised promises?

An advertised requirement is a condition that the user of a member function must comply with before the member function can be used. Some people use the term precondition instead of the term advertised requirement.

An advertised promise is a guarantee that a member function makes to its users. Some people use the term postcondition instead of the term advertised promise.

When a user fails to fulfill the advertised requirements of a member function, the member function usually fails to fulfill its advertised promises. For example, part of the advertised promise for myStack.pop() is that the number of elements will decrease by one. Therefore the advertised requirements for myStack.pop() will include !myStack.isEmpty() since myStack.pop() cannot fulfill this promise when myStack.empty() returns true.

When users fail to fulfill the advertised requirements, member functions normally throw exceptions. It is also legal, although often less desirable, for member functions to respond more severely to users who fail to fulfill the advertised requirements. For example, if Stack::pop() were invoked in a performance-critical path of the system, testing the advertised requirement might prove to be too expensive, in which case Stack::pop() might advertise, “If the Stack isEmpty(), arbitrarily disastrous things might happen.” This is a trust issue: such a statement in the requirement makes it the user's responsibility to guarantee that the preconditions are met prior to the call. This is normally done for performance purposes, where the number of redundant tests and/or the added complexity of those tests would be prohibitively expensive.

FAQ 6.05 How are the advertised requirements and advertised promises of the member functions specified?

Through the disciplined and consistent use of comments in the class declaration. For example, each member function can have the following lines just before or just after the declaration:

// PURPOSE: Tells users the overall purpose of the member function.

// REQUIRE: Tells users the advertised requirements. Since these must make sense to users, the text in this section should refer only to parameters and public: member functions.

// PROMISE: Tells users the advertised promises. Since these must make sense to users, the text in this section should refer only to parameters and public: member functions.

This technique is illustrated in the following example:

image

image

image

Keeping a class's specification in the header file (for example, the .hpp file) makes it easier for developers to find the specification and keep it synchronized with the code when changes are needed.

FAQ 6.06 Why are changes feared in development organizations that don't use specification?

image

Because no one knows what will break.

Changes come in three flavors.

Type 1: Implementation changes that do not change the interface specification

Type 2: Interface changes that are substitutable (that is, backward compatible)

Type 3: Interface changes that are nonsubstitutable (that is, non-backward compatible)

Changes of type 1 and type 2 are relatively cheap compared with changes of type 3, since changes of type 1 and type 2 cannot break user code that relies only on the specification; changes of type 3 can break user code.

In particular, there is often an enormous ripple effect when nonsubstitutable (that is, non-backward compatible) changes are made to a software application's interfaces. In some cases organizations can spend more time adjusting their old code than writing new code. This is especially true for organizations that are building large, complicated systems or applications. Because of this, it is important for developers to know what kind of changes they are making.

With proper specification, anyone (including maintenance programmers) can easily determine whether a proposed change to an interface will break existing code that uses that interface. Read on for more details.

FAQ 6.07 How do developers determine if a proposed change will break existing code?

The specification.

Unfortunately there is often an enormous ripple effect when nonsubstitutable (that is, non-backward compatible) changes are made to a software application's interfaces. In some cases organizations can spend more time adjusting their old code than writing new code. This is especially true for organizations that are building large, complicated systems or applications.

Developers should therefore be somewhat cautious of the difference between a substitutable change and a change that will break existing user code.

With proper specification, anyone (including maintenance programmers) can easily determine whether a proposed change to an interface will break existing code that uses the interface. Ill-specified systems typically suffer from “change phobia”: if anyone even contemplates changing anything, everyone starts sending out their résumés for fear that the system will collapse. Unfortunately, changes often do make the world fall apart in ill-specified systems. It's called maintenance cost and it eats software organizations alive.

FAQ 6.08 What are the properties of a substitutable (backward compatible) change in a specification?

Require no more, promise no less.

A change to an interface doesn't have to break existing code that uses the interface. If the new specification is substitutable with respect to the old specification, the user code will not break. Substitutable changes have two distinct properties. First, any user who fulfilled the old advertised requirements still fulfills the new ones (thus the new requirements must not get stronger). Second, any member function that fulfills the new advertised promises also would have fulfilled the old ones. In other words, existing user code must be adjusted if the replacement class requires users to do more and/or promises less than the original class did.

For example, Mac agrees to mow Lonnie's lawn for $10. Mac could substitute his friend, Patches, if the requirements went down (say $5) or if the promises went up (weeding the garden, for instance). However, Lonnie would be justifiably upset if Patches required more (say $20) or promised less (to mow only half the lawn, for instance).

In the following example, Version1 and Version2 represent subsequent versions of some class. Version2 is substitutable for Version1 since Version2 requires no more and promises no less than Version1 (in fact, Version2 requires less and promises more).

image

For Version2 to be substitutable for Version1, every member function must be substitutable. That is, every member function for the new version must require no more and promise no less than the equivalent member function in the old version. If even one member function is changed such that it requires more or promises less, the entire class is not substitutable.

FAQ 6.09 How can it be shown that the implementation of a member function fulfills its specification?

image

The implementation requires no more than the advertised requirements of its specification and delivers no less than the advertised promises of its specification.

The implementation of a member function doesn't have to be as picky as its advertised requirements. Also, a member function can deliver more than the minimum specified by its advertised promises. In the following example, the actual behavior of Fred::f() is different from its advertisement but different in a way that is substitutable and thus won't surprise users.

image

A specification is said to be adaptable when it is vague enough that an implementation may actually require less than its advertised requirements or when an implementation may actually deliver more than its advertised promises. Adaptable specifications are common in base classes since the adaptability gives extra latitude to derived classes.

FAQ 6.10 Is it possible to keep the specification synchronized with the code?

image

It's challenging, but it's doable.

On projects ranging from very small (10K lines of OO/C++) to very large systems (millions of lines of OO/C++) our experience has shown that this is a solvable problem.

Here are some guidelines.

All developers must be trained to properly specify all classes and member functions.

• The specifications must be kept with the class declarations (usually in a header file) so that it is easy for developers and users to refer to them.

• All users must be trained to rely on specifications rather than implementations.

• Everybody must treat a specification error as a defect that must be fixed with the same urgency that code defects are fixed.

• Everybody must be trained to recognize the differences between substitutable changes and nonsubstitutable changes.

It is possible to keep the specification synchronized with the code because of the nature of specifications. Since specifications describe behavior in terms that are observable to the user, rather than in terms of the implementation, specifications tend to be relatively stable compared with the implementation. Furthermore, since all the other software components of the system are built by relying on the specification rather than the implementation (see FAQ 6.09), the specifications tend to stabilize fairly early in the product life cycle. So as a practical matter, the problem isn't keeping the specification synchronized with the code but the reverse: making sure the code is synchronized with the specification. In other words, once a lot of software has been written based on a particular specification, that specification is more stable than the underlying code that implements the specification, so the issue is making sure the code faithfully implements the specification rather than the other way around.

Remember: specifications are prescriptive rather than descriptive. Specifications prescribe what the code must do rather than describe what the code already does. If the implementation of a member function and the member function's specification disagree, and if a lot of software was already built based on the specification, it is often easier to change the implementation to be consistent with the specification rather than the other way around.

In contrast, when there is no explicit specification that programmers can rely on, the implementation ends up being the de facto specification. Once that happens any change to the implementation (including fixing bugs in some cases!) can break the user code. In those cases it is trivial to keep the specification and the implementation in sync (the implementation is the specification), but it makes the whole application brittle—any change anywhere can trigger dozens of other changes.

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

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