Asserts and Architecture
From Software testing and development
Software Defects and hence Assert's certainly do not exist independently of notions of Good Architecture. Let's look at some classic ones and see what they imply about our use of asserts. See Reference [3] from Object Mentor.
The Acyclic Dependencies Principle
The dependency structure between packages must be a Directed Acyclic Graph (DAG). That is, there must be no cycles in the dependency structure.
The Stable Dependencies Principle (SDP)
The dependencies between packages in a design should be in the direction of the stability of the packages . A package should only depend upon packages that are more stable that it is .
A stable system obeys the Stable Dependencies Principle. Thus the client code is usually more unstable, fragile, newer, buggier. Hence...
The Heirarchy of Trust Rule
We should have less trust in (and hence more asserts on) data coming from code in higher layers than on data from code in the same or lower layers.
Example: In the following which values should we trust more, which values should we be paranoid about?
void service( int incoming)
{
int lower;
..// do stuff
lower = subservice( someExpression);
..// do more stuff
}
The value in variable “incoming” or the value in “lower”?
Hopefully the subservice is a simpler, more stable, more tested entity. So while it's reasonable to assert about both variables, checking “incoming” has priority.
Contents |
[edit] Defendable Programming.
“Defensive Programming” is the habit of checking everything before doing anything. Symptoms of this disease is unreadable code that contains more error checks and error reports than working code!
A far better idea is to do “Defendable Programming” by Data Encapsulation and Information Hiding.
Basically there are two things that all error checking code is trying to do:
- Stop the client from making a mess of things, and
- check that this module itself isn't breaking things.
To stop the client from making a mess is simple. Hide all your internal data structures so your clients cannot screw them up because they simply can't even see them.
You would never give your wallet to a shop keeper, so why do you expect your clients to manipulate your data structures directly?
By creating a narrow (and hence defendable) interface to your code, you limit what needs to be checked.
The one architectural design principle that above all others permits “Defendable Programming” is the
Law of Demeter (Kindergarden version)
- You can play with yourself.
- You can play with your own toys (but you can't take them apart),
- You can play with toys that were given to you.
- And you can play with toys you've made yourself.
Law of Demeter
- Your method can call other methods in its class directly
- Your method can call methods on its own fields directly (but not on the fields' fields)
- When your method takes parameters, your method can call methods on those parameters directly.
- When your method creates local objects, that method can call methods on the local objects.
To stop your own code from making a mess of things, you need to work out what you are trying to protect, what are you trying to ensure, what constraints are you trying to enforce.
Perhaps a simpler way of thinking about it is to make your classes and modules sealed “Bit Boxes”. The only way to modify or access the state they contain is via their formally declared and narrow by design public interface.
Create Bit Boxes.
Encapsulate state so it cannot causally effect other parts of the system, nor be modified except directly via a narrow declared public interface
[edit] Class Invariants.
A class invariant is a boolean expression that is always true at the end of the initialization routine, and at the start and end of every public method.
Bjarne Strostrup, the designer of the C++ language says :
“You should create a class if and only if you have an invariant to protect.”
In C, the notion of a class more or less conforms to the notion of a compilation module.
A good way of finding invariants is to look at the state held by this module seeking...
- constraints like bounds on indices, and address ranges for pointers on variables internal to your module.
- things that must vary together, e.g., if this changes so must that.
- things that must always be true if this module is to work correctly.
You can sweep all these assertions into a private “check_invariant” method which you can call at the start and end of every public method.
If the internal state of a module is not co-related in some way it is a strong hint that your module lacks cohesion and should be split into two or more smaller modules. It it complete lacks cohesion, then perhaps it is a module, but a Plain Old Data struct.
[edit] What should go into a PRE/POST or INVARIANT assert?
A precondition states what a client should undertake to set up. If the expression involves variables encapsulated by the module it implies
- The client must be excessively aware of the internal state of the object. (i.e., the information is not properly hidden)
- There is a strict and hidden ordering on the way the public methods are invoked.
A better approach is to design the interface and encapsulation so ...
Design Stateless Interfaces...
- the precondition is a function only of the function parameters,
- any other preconditions on the internal state should be automagically guaranteed by the invariant.
- The invariant expression should only be a function of the encapsulated state.
Failure to comply with the Law of Demeter or the Bit Box approach can result in an objects invariant being smashed by client code. Getters and Setters leak the encapsulation making it impossible for the class to protect its invariant.
A Bank contracts with you to preserve your Bank balance according to certain rules. If any Joe Soap can grab things out of, or put things directly into the Bank's books, there is no way the Bank can maintain integrity.
A class contracts to preserve its internal state according to agreed on rules, doing its part if you do yours.
A class cannot maintain its integrity if any part of the system can alter that state.
A class cannot gaurantee its state to be consistent if other parts of the system can change its state by means other than the public API.
[edit] Asserts and Layered Software.
Assume the following call graph....
void A( int i)
{
// Some stuff
B(i);
}
void C( int i)
{
// Do other stuff
B(i);
}
void B(int i)
{
D(i);
}
void D(int i)
{
for( int j = 0; j < lots; ++j) {
E(i);
}
}
void E(int i)
{
// Do stuff that requires i > 0
}
Where do you place the precondition assert(s)?
Let's consider the options...
- Everywhere.
This means i gets tested against the same constraint 3 times on every call. Recommendation : Don't do this. - At the top level in function A and C.
This means the fault is found early, but the fact “i must be greater than zero” appears in two places, both far from the code that has the actual constraint.
Recommendation: Only do this enroute to performing a hazardous operation, resulting in less that needs to be put back to a safe mode. - At the start of the first common function B.
Pro:, fault gets caught early, the constraint is documented (by the assert) at a higher level.
Con: The constraint is documented and enforced at a distance from where the constraint is.
Recommendation: Don't do this. - The assert placed at the highest level where at which things actually go wrong if constraint not met.
ie. In function E.
Pro: Constraint and Assert live together and are easier to change together, one fact one place.
Con: Assert now lives in an inner loop.
Recommendation: Do this until profiling / inspection demonstrates you mustn't. "We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil."- Options thereafter are:
- Move the assert one level up to start of function D.
- Make E an inline function and trust to the compilers optimizer to move constant sub expressions out of the loop.
- Options thereafter are:
These recommendations can be summed up as...
The One Fact One Place, Don't Repeat Yourself DRY rule.
Keep the assert as near as is reasonable to the place where the constraint actually pinches.
- Trust on your systems ability to do backtraces to show you how you got into that invalid state. (See reference 7, eCrash)
- If your assert is far from the constraint you risk introducing assert bugs as the code evolves and people forget to update the asserts to match the new constraints.
- Constructors / Initializers are an exception to this rule. Often an invalid parameter will be stashed in an instance variable. If the constraint only pinches in a subsequent method call, the bug will not be on the backtrace.
Previous: Design by Contract Next: Assert failures
