Our failure to master the complexity of software results in projects that are late, over budget, and deficient in their stated requirements.
1. The Inherent Complexity of Software
“Einstein argued that there must be simplified explanations of nature, because God is not capricious or arbitrary[haphazard]. No such faith comforts the software engineer. Much of the complexity that he must master is arbitrary complexity”.
The main characteristic of industrial-strength software is that:
– it is intensely difficult,
– if not impossible, for the individual developer to understand all the subtleties [precision] of its design.
In direct terms, the complexity of such systems exceeds the human intellectual capacity.
“The complexity of software is an essential property, not an accidental one”.
We observe that this inherent complexity derives from four elements:
The problems we try to solve in software often involve elements of unavoidable complexity, in which we find a myriad of competing, perhaps even contradictory, requirements.
Consider the requirements for the electronic system of a multi-engine aircraft, a cellular phone switching system, or an autonomous robot.
The raw functionality of such systems is difficult enough to figure out, but now add all of the (often implicit) nonfunctional requirements such as usability, performance, cost, survivability, and reliability.
This unrestrained external complexity is what causes the arbitrary complexity.
This external complexity usually springs from the “communication gap” that exists between the users of a system and its developers:
Users generally find it very hard to give precise expression to their needs in a form that developers can understand.
In some cases, users may have only vague ideas of what they want in a software system. This is not so much the fault of either the users or the developers of a system; rather, it occurs because each group generally lacks expertise in the domain of the other.
Users and developers have different perspectives on the nature of the problem and make different assumptions regarding the nature of the solution.
The fundamental task of the software development team is to engineer the illusion of simplicity.
Certainly, size is no great virtue in a software system. We strive to write less code by inventing clever and powerful mechanisms that give us this illusion of simplicity.
How- ever, the absolute volume of a system’s requirements is sometimes unavoidable and forces us either to write a large amount of new software or to reuse existing software in novel ways.
Today, it is not unusual to find delivered systems whose size is measured in hundreds of thousands or even millions of lines of code (and all of that in a high-order programming language, as well).
No one person can ever understand such a system completely. Even if we decompose our implementation in meaningful ways, we still end up with hundreds and sometimes thousands of separate modules.
This amount of work demands that we use a team of developers, and ideally we use as small a team as possible.
Also having more developers means more complex communication and hence more difficult coordination, particularly if the team is geographically dispersed.
However, no matter what its size, there are always significant challenges associated with team development.
A home-building company generally does not operate its own tree farm from which to harvest trees for lumber.
Yet in the software industry such practice is common. Software offers the ultimate flexibility, so it is possible for a developer to express almost any kind of abstraction.
This flexibility turns out to be an incredibly seductive property, however, because it
also forces the developer to craft virtually all the primitive building blocks on which these higher-level abstractions stand.
While the construction industry has uniform building codes and standards for the quality of raw materials, few such standards exist in the software industry.
As a result, software development remains a labor-intensive business.
We would be very surprised if just because we threw the ball a little harder, halfway through its flight it suddenly stopped and shot straight up into the air.
In a not-quite-debugged software simulation of this ball’s motion, exactly that kind of behavior can easily occur.
Within a large application, there may be hundreds or even thousands of variables as well as more than one thread of control.
The entire collection of these variables, their current values, and the current address and calling stack of each process within the system constitute the present state of the application. Because we execute our software on digital computers, we have a system with discrete states.
Discrete systems by their very nature have a finite number of possible states; in large systems, there is a combinatorial explosion that makes this number very large.
We try to design our systems with a separation of concerns, so that the behavior in one part of a system has minimal impact on the behavior in another.
However, the fact remains that the phase transitions among discrete states cannot be modeled by continuous functions. Each event external to a software system has the potential of placing that system in a new state, and furthermore, the mapping from state to state is not always deterministic. In the worst circumstances, an external event may corrupt the state of a system because its designers failed to take into account certain interactions among events.