Advanced Software Engineering
Part 08 – Avoiding Debugging
Dr. Amjad AbuHassan
2/21/24 Dr. Amjad AbuHassan 1
Objectives
● Write code that either avoids debugging entirely,
● or at least makes it easy when we must do it.
2/21/24 Dr. Amjad AbuHassan 2
First defense: make bugs impossible
● The best defense against bugs is to make them impossible by design.
● Static checking eliminates many bugs by catching them at compile
time.
● Dynamic checking.
● Immutability is another design principle that prevents bugs.
● Unreassignable references: variables declared with the keyword final
2/21/24 Dr. Amjad AbuHassan 3
Example
final char[] letters = new char[] { 'a', 'e', 'i', 'o', 'u' };
● The letters variable is declared final, but is it really unchanging?
● Which of the following statements will be illegal (caught statically by
the compiler), and which will be allowed?
letters = new char[] { 'x', 'y', 'z' };
letters[0] = 'z';
2/21/24 Dr. Amjad AbuHassan 4
Second defense: localize bugs
● Localize bugs to a small part of the program if we can’t prevent them
● When localized to a single method or small module
● bugs may be found simply by studying the program text.
● the easier it is to fix
/**
* @param x requires x >= 0
* @return approximation to square root of x
*/
public double sqrt(double x) { ... }
2/21/24 Dr. Amjad AbuHassan 5
Example 2
● Suppose we calls sqrt with a negative argument.
● What’s the best behavior for sqrt?
● Since the caller has failed to satisfy the requirement that x should be
nonnegative, sqrt is no longer bound by the terms of its contract, so it
is technically free to do whatever it wants:
● return an arbitrary value, or enter an infinite loop, or melt down the CPU.
2/21/24 Dr. Amjad AbuHassan 6
Example 2 cont.
● The most useful behavior would point out the bug as early as possible,
by inserting a runtime check of the precondition.
/**
* @param x requires x >= 0
* @return approximation to square root of x
*/
public double sqrt(double x) {
if (! (x >= 0))
throw new IllegalArgumentException("required x >= 0, but was: " + x);
...
}
2/21/24 Dr. Amjad AbuHassan 7
Example 2 cont.
● When the precondition is not satisfied, this code terminates the
program by throwing an unchecked IllegalArgumentException.
● The effects of the caller’s bug are prevented from propagating.
● Checking preconditions is an example of defensive programming.
● Defensive programming offers a way to mitigate the effects of bugs
even if you don’t know where they are.
2/21/24 Dr. Amjad AbuHassan 8
Assertions
● assert is a built-in statement in the language, not a method .
● The simplest form: takes a Boolean expression and throws
AssertionError if the Boolean expression evaluates to false:
assert x >= 0;
● Assertions have the added benefit of documenting an assumption
about the state of the program at that point,
● assert x >= 0 says “at this point, it should always be true that x >= 0.”
2/21/24 Dr. Amjad AbuHassan 9
Assertions cont.
● An assertion is executable code unlike a comment,
● Assertion may also include a description expression, which is usually a
string, but may also be a primitive type or a reference to an object.
assert x >= 0 : "x is " + x;
● If x == -1, then this assertion fails with the error message
● x is -1
2/21/24 Dr. Amjad AbuHassan 10
Assertions cont.
● Along with a stack trace that tells you where the assertion was found in
your code and the sequence of calls that brought the program to that
point.
● This information is often enough to get started in finding the bug.
2/21/24 Dr. Amjad AbuHassan 11
Assertions Problem
● A serious problem with Java assertions is that assertions are off by default.
● If you just run your program as usual, none of your assertions will be checked!
● Java’s designers did this because checking assertions can sometimes be costly to
performance.
● For example, a function that searches an array using binary search has a
requirement that the array be sorted.
● Asserting this requirement requires scanning through the entire array.
2/21/24 Dr. Amjad AbuHassan 12
Assert vs JUnit Assertions
● Java assert statement is a different mechanism from the JUnit
methods assertTrue(), assertEquals(), etc.
● They all assert a predicate about code but are designed for use in different
contexts.
● The assert statement: used in implementation code, for defensive
checks inside the implementation.
● JUnit assert...() : used in JUnit tests, to check the result of a test.
● The assert statements don’t run without -ea, but JUnit assert…() always run.
2/21/24 Dr. Amjad AbuHassan 14
What to Assert
Some things you should assert:
● Method argument requirements, like in sqrt example.
● Method return value requirements. This kind of assertion is
sometimes called a self check.
● For example, the sqrt method might square its result to check whether it is
reasonably close to x:
2/21/24 Dr. Amjad AbuHassan 15
What to Assert cont.
● Write runtime assertions as you write
public double sqrt(double x) {
the code, not after the fact. assert x >= 0;
double r;
● you will have the invariants in mind. ... // compute result r
● If you postpone writing assertions, assert [Link](r*r - x) < .0001;
return r;
you’re less likely to do it, and you’re }
liable to omit some important invariants.
2/21/24 Dr. Amjad AbuHassan 16
What not to Assert
● Runtime assertions are not free. They can clutter the code, so they
must be used with good judgment.
● Avoid trivial assertions, For example:
// don't do this:
x = y + 1;
assert x == y+1;
2/21/24 Dr. Amjad AbuHassan 17
What not to Assert cont.
● Never use assertions to test conditions that are external to your
program, such as
● the existence of files,
● the availability of the network,
● or the correctness of input typed by a human user.
● Assertions test the internal state of your program to ensure that it is
within the bounds of its specification.
2/21/24 Dr. Amjad AbuHassan 18
What not to Assert cont.
● Assertion failures indicate bugs.
● External failures are not bugs, and there is no change you can make to
your program in advance that will prevent them from happening.
● External failures should be handled using exceptions instead, like
FileNotFoundException or NoRouteToHostException.
2/21/24 Dr. Amjad AbuHassan 19
What not to Assert cont.
● Many assertion mechanisms are designed so that assertions are
executed only during testing and debugging and turned off when the
program is released to users.
● Since Java assertions may be disabled, program correctness should
never depend on whether or not the assertion expressions are
executed.
2/21/24 Dr. Amjad AbuHassan 20
What not to Assert cont.
● Asserted expressions should not have side-effects. For example, if we want
to assert that an element removed from a list was actually found in the list,
we don’t write it like this:
// don't do this:
assert [Link](x);
● If assertions are disabled, the entire expression is skipped, and x is never
removed from the list. We write it like this instead:
boolean found = [Link](x);
assert found;
2/21/24 Dr. Amjad AbuHassan 21
What not to Assert cont.
● If a conditional statement or switch does not cover all the possible
cases, it is good practice to use a check to block the illegal cases.
● We don’t use the assert statement here, because it can be turned off.
● Instead, throw an exception in the illegal cases, so that the check will
always happen:
2/21/24 Dr. Amjad AbuHassan 22
What not to Assert cont.
switch (vowel) {
case 'a’:
case 'e’:
case 'i’:
case 'o’:
case 'u': return "A";
default: throw new AssertionError("must be a vowel, but was: " + vowel);
}
2/21/24 Dr. Amjad AbuHassan 23
Incremental Development
● A great way to localize bugs to a tiny part of the program is
incremental development.
● Build only a bit of the program at a time, and test that bit thoroughly
before move on.
● When we discover a bug, it’s more likely to be in the part that we just
wrote, rather than anywhere in a huge pile of code.
2/21/24 Dr. Amjad AbuHassan 24
Incremental Development cont.
● Two techniques that help with this:
● Unit testing: when we test a module in isolation, we can be confident
that any bug we find is in that unit – or maybe in the test cases
themselves.
● Regression testing: when we are adding a new feature to a big system,
run the regression test suite as often as possible. If a test fails, the bug
is probably in the code you just changed.
2/21/24 Dr. Amjad AbuHassan 25
Modularity & Encapsulation
● We can localize bugs by better software design.
● Modularity means dividing up a system into components, or modules,
each of which can be designed, implemented, tested, reasoned about,
and reused separately from the rest of the system.
● The opposite of a modular system is a monolithic system – big and with
all of its pieces tangled up and dependent on each other.
2/21/24 Dr. Amjad AbuHassan 26
Modularity & Encapsulation cont.
● Encapsulation means building walls around a module so that the
module is responsible for its own internal behavior, and bugs in other
parts of the system can’t damage its integrity.
● One kind of encapsulation is access control, using public and private to
control the visibility and accessibility of the variables and methods.
● A public variable or method can be accessed by any code (assuming the
class containing that variable or method is also public).
2/21/24 Dr. Amjad AbuHassan 27
Modularity & Encapsulation cont.
● A private variable or method can only be accessed by code in the same
class.
● Keeping things private as possible, it limits the code that could cause bugs.
● Another kind of encapsulation comes from variable scope.
● The scope of a variable is the portion of the program text over which
that variable is defined
2/21/24 Dr. Amjad AbuHassan 28
Modularity & Encapsulation cont.
● A method parameter’s scope is the body of the method. A local
variable’s scope extends from its declaration to the next closing curly
brace.
● Keeping variable scopes as small as possible makes it much easier to
reason about where a bug might be in the program. For example,
suppose we have a loop like this:
for (i = 0; i < 100; ++i) {
...
doSomeThings();
...
}
2/21/24 Dr. Amjad AbuHassan 29
Modularity & Encapsulation cont.
● … and you’ve discovered that this loop keeps running forever – i never
reaches 100. Somewhere, somebody is changing i. But where? If i is
declared as a global variable like this:
public static int i;
...
for (i = 0; i < 100; ++i) {
...
doSomeThings();
...
}
2/21/24 Dr. Amjad AbuHassan 30
Modularity & Encapsulation cont.
● … then its scope is the entire program. It might be changed anywhere
in the program: by doSomeThings(), by some other method
that doSomeThings() calls, by a concurrent thread running some
completely different code.
● But if i is instead declared as a local variable with a narrow scope:
for (int i = 0; i < 100; ++i) {
...
doSomeThings();
...
}
2/21/24 Dr. Amjad AbuHassan 31
Modularity & Encapsulation cont.
● Minimizing the scope of variables is a powerful practice for bug
localization. Here are a few rules that are good for Java:
● Always declare a loop variable in the for-loop initializer.
● Declare a variable only when you first need it, and in the innermost curly-brace
block that you can
● Avoid global variables
2/21/24 Dr. Amjad AbuHassan 32
Minimizing the Scope of Variables
● Always declare a loop variable in the for-loop initializer. So rather
than declaring it before the loop:
int i;
for (i = 0; i < 100; ++i) {
● which makes the scope of the variable the entire rest of the outer
curly-brace block containing this code, we should do this:
for (int i = 0; i < 100; ++i) {
● which makes the scope of i limited just to the for loop.
2/21/24 Dr. Amjad AbuHassan 33
Minimizing the Scope of Variables cont.
● Declare a variable only when you first need it, and in the innermost
curly-brace block that you can. Variable scopes in Java are curly-brace
blocks, so put the variable declaration in the innermost one that
contains all the expressions that need to use the variable.
● The languages without static type declarations, like Python and
JavaScript, the scope of a variable is normally the entire function, so
we can’t restrict the scope of a variable with curly braces, alas.
2/21/24 Dr. Amjad AbuHassan 34
Minimizing the Scope of Variables cont.
● Avoid global variables. Using global variables is a very bad idea,
especially as programs get large.
● It’s better to just pass the parameter into the code that needs it, rather
than putting it in global space where it can be reassigned.
2/21/24 Dr. Amjad AbuHassan 35