The style used in this essay is deliberately pretentious, almost comic-epic. I have tried to lampoon the pompous proclamations of software engineering gurus; phrases and speech mannerisms of pundits and well-known practitioners are used and abused without remorse throughout. In anticipation of misconstruction on the part of sensitive souls, I have written a short account of the gestation of LSD-Meth, which I hope will offend a different set of people and which I would recommend you read first. So… try to take it easy as you read on: this is serious stuff! ^_^
Table of Contents
Applicability
This development methodology is applicable to projects in which the following criteria are met:
- the feature scope is (or can be) dynamic;
- the software architecture does not yet exist; and
- the overhead of interpersonal communications is low.
Values
A project that employs this methodology is driven by the following values:
- Consistency
- A particular implementation of a feature can be said to have an intrinsic value that depends, in part, on its observance of the values driving the whole of the development effort.
- Reliability
- Unexpected or erratic behavior is, with very few exceptions, unacceptable. Ordinary functionality and mediocre performance that you can count on are, in any case, preferable to extraordinary functionality and spectacular performance that you can only enjoy for a few seconds at a time.
- Simplicity
- An interface must have the simplest design that supports the features required for the immediate production target, and an interface's implementation must have the simplest design that supports the functionality specified by said interface.
- Correctness
- The implementation of an interface is correct, however simple, as long as said implementation complies with the design contract implied by the interface definition. An interface is correct, however simple, when it delivers the functionality specified by the feature's requirements.
- Orthogonality
- Duplicate logic in the system should only be tolerated when doing so is necessary in order to satisfy one of the aforementioned values.
Techniques
The following supporting practices must be used:
- Aggressive Scope Management
- If we constrain the variability of cost and quality, scope is then the only effective parameter whereby to assert control of the project's pace. Given the possibility (yea, the likelihood) that the desired feature set or the project timeline may change unexpectedly, the safest approach to delivering the most valuable system at any given time is making most valuable of the immediately attainable features the current concern. (Also, see Continuous Production.) For example, when no code has yet been written, a program that takes a symbol representing a data source as an argument (whether it be a file name or a URL) and returns a suitable code upon termination represents an immediately attainable production target; such a program has the virtue of introducing the most valuable feature a program can claim: successful execution.
- Continuous Production
- The purpose of each release cycle is to enhance the functionality of the production system, and the goal of each build cycle is to yield a deliverable system. If at all possible, a release cycle should be as short as a single build cycle. The combination of aggressive scope management and continuous production facilitates timeliness and simplicity. We wish to deliver a valuable system as early as possible and, thereafter, we wish to deliver a new, more valuable system as often as possible; therefore, we wish to make only the smallest significant (i.e., valuable) change to the system that results in a new, deliverable system.
- Defensive Programming
- There must be no unadvertised program behavior; for example, abnormal program termination and runtime selection of algorithms (such as might be warranted to accommodate changes in available resources) is always accompanied by suitable diagnostic information. (Also, see Continuous Testing.) Defensive programming encompasses many well-known maxims: the program must verify that the value of a type instance lies within its formal domain; the program must verify that a type instance exists (or can exist) at a given memory location before addressing it or any of its constituent elements; the program must notify the user or hosting environment of runtime changes in behavior, such as might be warranted to accommodate changes in available resources; when a show-stopping event is intercepted by defensive code, diagnostic information should accompany the graceful termination of the program; etc.
- Continuous Testing
- During each build cycle, compliance with the design contract (correctness) is verified by means of automated tests; thus, it is always possible to learn what features are correctly implemented and, consequently, what the net value of the system is. The facilitation of continuous testing should be kept in mind at all times; for example, it is well worth the effort to write a test driver for each implementation of an interface so that a testing framework can exercise the functionality specified by the interface whenever the code is compiled. The combination of pervasive defensive programming and continuous testing drives correctness, reliability, and measurability.
- Interface-Based Design
- Requirements will be precisely specified in the form of implementation-independent interfaces written in a suitable interface definition language, which could be (but is not necessarily) the implementation language. Sketching a valid C language header file containing magic constants and function prototypes for a library that will provide a particular feature is a fine example of interface-based design.
- Continuous Refactoring
- As soon as duplicate logic is detected in the system, the code from all such instances must be consolidated and repackaged in such a way that all corresponding dependencies are also consolidated, which results in the creation of a new, genuinely (albeit, internally) reusable interface. The consolidation of ad hoc assertion and exception handling code from various modules into a shared module is a good example of that especially desirable kind of refactoring which results in the implementation of a reusable interface. The combination of interface-based design and continuous refactoring makes it possible to uphold the value of orthogonality.
- Iterative Project Scheduling
- A proposal describing the features to be implemented and setting a target date for completion of the work must be drafted for each release cycle. Release cycles should be brief. The numbering scheme for release cycles, which is based on the revision number of the project requirements document, may be decoupled from that of build cycles, which is based on the revision number of the program source files. Upon delivery, the release must be accompanied by documentation describing examples of program usage and specifying the degree and manner of compliance with the cumulative requirements. A discussion of the implementation process is optional.
Discussion
The aforementioned practices and values constitute a genuine discipline of software engineering (they are all necessary and, together, sufficient) that addresses the most troubling software engineering issues outside the realm of interpersonal communication. This audacious claim warrants some clarification. If you have read (and believed) at least one paper in which the author claims that simplicity and correctness in software are at odds, the notion that both should be cardinal values may seem like madness and the subordination of simplicity to correctness could seem like hypocrisy. While it may be true that it is often impossible to produce a satisfyingly simple implementation of a correct design specification, it is almost always possible to produce a completely correct implementation of a simple design specification; in other words, the best you can think of is seldom immediately attainable, whereas a simple working system with demonstrable value is often immediately attainable.
This has profound implications for a project's survival: a mediocre production system can be redesigned and reimplemented incrementally, and its value increased gradually until it is functionally indistinguishable from the best you could think of, but a correct design is worthless if the system does not attain production. Upon hearing this, many software engineering gurus would claim that evolution into a correct form is not possible because the cost of changes to a system's implementation and architecture increase exponentially with time, especially after the system attains production; I contend that the belief that changes to the production system will be prohibitively expensive leads software architects to overdesign at the earliest possible stage, which in turn soon fulfills the expectation that the production system will have so much architectural inertia as to be immutable.
In order for a gradual evolution of a production system to be feasible, we must resist the temptation to design interfaces that are more complex than is needed to fulfill the current production target's requirements, especially in the name of reusability. Additionally, in order for maximize the value of the deliverable system, we must implement the most valuable features first, and integrate new features as quickly as possible — lest the system be delivered before we do.