Software Design/Code robustness
Interface robustness characterizes how easy or hard it is to introduce a bug in code using the interface when that code is originally written, or while changing the code. Introducing a bug in these definitions means not just an insertion of a bug, but an insertion of a bug that won't be caught during all code quality checks and will go into production. For example, it may be relatively easy to insert a bug into code written in some dynamically typed language, but if the code has an extensive test suite, most of such bugs would be caught during testing. In this case, the codebase should be considered robust overall.
Different "levels of robustness" may be defined depending on at which stage of software quality checking most errors are surfaced: compilation, linting/static analysis, unit testing, integration testing, code review, etc. Software design practices that aid bug discovery during the quality control stages which are performed more frequently and/or sooner after coding (the moment of the bug insertion) may be considered to ensure "stronger" code or interface robustness.
Types of software errors
editBoehm, McClean, and Urfrig identify four types of software errors:[1]
- Communication: errors due to miscommunication of interface (API) contracts, requirements, assumptions.
- Completeness: errors due to incomplete grasp of requirements and contracts.
- Consistency: conceptual errors implementing requirements or coding against interface (API) contracts.
- Clerical: usually, typographical errors, such as using a wrong constant, mistyping
+
sign instead of-
in a formula, etc.
This taxonomy of errors is used below in this article.
Examples
editNamed parameters
editIf the programming language supports named parameters, their usage (albeit optional) reduces the risk of confusing the order of passed arguments (a clerical error):
val space = Rect(length=maxLength, width=maxWidth)
// or
val space = Rect(maxLength, maxWidth)
In the first version of the statement, would be hard to not notice a error if the maxLength
and maxWidth
arguments were provided in the wrong order. But it would be easy to miss such a error during development and code review with the second version of the statement.
Safe builders
editBuilder pattern, among other things, makes the code more evident because each parameter of the object being constructed is configured via a function with a name, which is not achievable with constructors in languages which don't support named parameters. On the other hand, builder creates a room for completeness errors: a developer may forget to configure some parameters. Accompanying each field with a boolean flag provides runtime protection against this type of error:
class CarBuilder {
private boolean wheelSet = false;
private int wheel;
private boolean colorSet = false;
private String color;
public CarBuilder setWheel(int wheel) {
this.wheelSet = true;
this.wheel = wheel;
return this;
}
public CarBuilder setColor(String color) {
this.colorSet = true;
this.color = color;
return this;
}
public Car build() {
if (!wheelSet) throw new IllegalStateException("wheel should be set");
if (!colorSet) throw new IllegalStateException("color should be set");
return new Car(wheel, color);
}
}
This could be simplified when there are some values of the field type not allowed for the field, e. g. -1
for wheel
and null
for color
in the Java example above.
Type-safe builder pattern[2] ensures compile-time robustness against completeness errors.
Design by contract
editThe practices of verifying function's inputs (preconditions), outputs (postconditions), and class invariants in code (e. g. using assertions) combined with automated testing (such as unit or integration testing) helps to surface communication and consistency errors associated with using functions, objects, APIs, and components.
RAII
editRAII is a technique which doesn't leave room for completeness errors of developer forgetting initialize an object by calling a separate function after creating it.
Mutation testing
editMutation testing is a testing practice that helps to protect against completeness errors in test code itself: for example, forgetting to verify the result of a call to a function under test, or forgetting to test a certain aspect of the behavior of the function or the class at all.
Relations to other qualities
editIf a function's or class's implementation is highly complex then developers are more likely to make consistency and completeness errors. For example, in the implementation of a complex interaction protocol, the order of two steps could be confused.
It may be easier to spot completeness, consistency, and clerical errors in highly structured code because these errors will break the visual pattern of the code. A developer or reviewer can mentally skip through the repetitive structure and focus only on the critical details.
Bugs can also be found more easily in clearer code because there are less distracting details.
If the meaning of some code is not obvious, reviewers are more likely to skim through this code without really trying to verify its semantics mentally, so the chances increase that there are some latent bugs remaining in such code.
When dependencies between code constructs are unapparent there is a higher risk for a developer to alter one of them without updating another (because the developer may be unaware of the dependency) and thus to introduce a communication or consistency error.
Change amplification makes bugs more likely in two ways. First, high change amplification makes changesets larger, which, in turn, are reviewed less effectively in terms of uncovering programming errors.[3] Second, if the compiler doesn't enforce change in all required places in the codebase, it's easier for a developer to forget making some of them when there are many.
When modifying code highly robust against programming error, developers may experience less background alertness because they know errors if happen will be caught by the compiler or the test suite so they don't need to constantly watch and check after themselves for not inserting bugs. Less alertness means less cognitive load.
Mistake tolerance
editCode robustness is at odds with mistake tolerance of the dependencies (i. e. functions and classes used in the code): if the dependency permits erroneous usage, by definition it's easier to insert bugs in the code using the dependency. The mistake tolerance of dependencies could be mitigated by some quality assurance practices such as static analysis, integration testing, and mutation testing, as well as an operations practice of making the dependency to log or alert erroneous usages (though tolerating it) and targeting zero such warnings or alerts in production operation of the system.
Relevant practices
edit- Design by contract: check (assert) function's preconditions, postconditions, state invariants
- Specify interface contracts
- Catch the most specific type of exception
- Hide an object access chain (in a concurrent environment)
- Share state by communicating
- Use class instead of primitive type
References
edit- ↑ Boehm, Barry W.; McClean, Robert K.; Urfrig, D. E. (March 1975). "Some experience with automated aids to the design of large-scale reliable software". IEEE Transactions on Software Engineering SE-1 (1): 125-133. doi:10.1109/tse.1975.6312826. https://ieeexplore.ieee.org/document/6312826.
- ↑ Rijo, Pedro (September 17, 2019). "Type safe builder pattern".
- ↑ Best Kept Secrets of Peer Code Review. 2013. p. 79. ISBN 978-1599160672. https://static1.smartbear.co/smartbear/media/pdfs/best-kept-secrets-of-peer-code-review_redirected.pdf. "Defect Density Analysis" section