Embracing the Abyss of Suspicion
It is “simple” in many ways to deconstruct an application. We do this as a matter of course in many refactoring efforts, and whenever we’re approaching an existing code base. We examine the pieces individually, or perhaps we trace through subsystems and interactions – this external action or detected state initiates this response in the code, prompting a cascade of effects and processes to themselves begin, often with a resulting calculation or response returns upwards out of the code.
..but what does it mean?
Code is written as a response to a human need. That need might be a report of profits and losses for the quarter, ordering queso deliveries at 3am, or delighting a child with edutainment, but these are all driven by a human wanting something. Naturally, the argument could be made that the world doesn’t need “Uber for cheese dip” and while you’d likely be correct, the human need in this case is driven by our economics; someone with money to invest has determined that the best use of their resources is to attempt to make a go out of on-demand fondue.
Code must exist outside of merely the mechanistic operation of a computer, and be valued within the context of it serving a given need. A piece of software that fails to meet this need can be said to be “poor,” and one that achieves it’s goal to be “good.” This is especially true once we take into account the fact that code must also exist within a context of multiple human needs, which sometimes compete. The needs of end users, the needs of the business, the needs of the developers, but also the needs of the code itself as a distinctly human creation that must be tended and groomed as its contexts shifts — all these ends must be served.
Thus, if you can’t consider what a thing means, and look only at what it is, you hobble your ability to understand the larger complex that thing exists within. Too often we work in a world of architects who generate reams of diagrams showing us the topology of a system. Valuable information, to be sure, and critical to the goal of architectural design, but too often there is precious little information about the how of a system, let alone the why. We are shown a static representation of the system, with no clues as to the flow of messages and interactions between it’s parts in response to stimuli. In an effort to catalog and organize the units of a system, we fail to explore it’s existence in Time, or tell the story of it’s interactions with us.
As we observe the interplay between these forces, of the code attempting to fulfill demands, along with the demands shifting as the code satisfies needs and perhaps reveals needs previously unknown needs, our ability to reason increases as facts about the function and purpose are revealed. Just as sculptor can be said to not be fashioning a statue from marble so much as removing excess marble to reveal the statue hidden inside the block, code can be said to have an idealized state in which it could be said to be “perfect” in serving in balance all the demands which have been placed upon it.
Rationally, we recognize that this can never actually be true; code can never be perfect, as the context for it continually shifts. This brings to mind the process of evolution, creating never a perfect species, but only perfectly evolved species for their environment in this moment. This small distinction contains the recognition that as time progresses, demands shift, people come and go, and we continually learn more and more about our craft. Weeks or months go by, and our perfect statue no longer seems to have been fully exposed; there are bits of marble left to remove, subtleties to the human need that weren’t served in our initial attempts that must now be addressed. As the environment changes, clinging to our beliefs about the relative correctness of the code without considering the new landscape of demands is a step towards technical doom.
We attempt to model our comprehension of the desire of the software with testing, to say “this code suits what I know about this human need.” Unit testing exists within the mechanistic state of “what does this thing do?” whereas integration or behavioral testing attempts to address the questions of “what does this mean?” and “how does this software exist within a dynamic environment?” As they strive to gain confidence and knowledge about literally the interactions and behaviors of objects, the begin to illuminate us as to the nature of the code within context. Both aspects must be examined, and it is a failure in ourselves that we can not bridge the gulf between these concepts – the static existence versus the dynamic. One common trait shared by more experienced developers is the ability to shift contextual viewpoint between these perspectives, to swap the ideology of the watchmaker-like world of unit testing, for the “30,000 foot view” ideology.
Each of these angles of testing directly serve to build a more detailed picture of the need we’re attempting to satisfy, and the manner in which we’re fulfilling it. As complexity grows however, it exposes the limits of our rationality. There simply exist too many states, too many interactions, for either our code or testing to be considered complete, and as more and more demands are placed upon the code, its purpose must ever more be at cross purposes with itself, its identity becoming muddled. How often have you built a pure API, only to realize that now you must add a reporting tool? ..an admin dashboard? ..some sort of data manipulation never once hinted at in the original problem statement? Rather than being “confident,” at best we can aspire to be “comprehensive” in our code and our tests, a word which itself is misleading and truthful, as if our testing could “comprehend” the code’s possibilities, rather it is at best merely a representation of our limited knowledge of known fail states.
Code exists as instructions, a series of reactions to stimuli. Thus, an error case then represents something new, something that the code does not have a prearranged response to. A bug occurs and we are pushed into a place of discovery. We are presented with proof of our insufficiencies, either as developers making an inevitable, human mistake in syntax, control flow, or other “duh” errors, or in our understanding of the process required for code to successfully achieve the goal we had in creating it.
This land of discovery increases our ability to reason about the nature of software, both in terms of the immediate code and the code as it exists into the future. In a sense then, if we accept that code is an expression of our understanding, all code must then exist as a single code base or repo, across projects and jobs and technologies. We are the single constant, bringing our comprehension of the nature of computational instructions to bear when generating solutions to human needs, which themselves exists within the boundaries of humanity. By understanding the rationality of software, as well as the human context in which its created, you multiply your ability to craft solutions that are increasingly resilient – but still, never perfect.
It is in the challenging of our starting point, pushing us from stasis to growth, that wisdom can begin to emerge. We engage with the Hero’s Journey and return with knowledge to slay the instance of failure, and our relationship to the wider context that has generated this failure is forever changed – fool me once can’t fool me again
Slavoj Žižek said:
Imagine ideology as a kind of filter, a frame, so that if you look at the same ordinary reality through that frame, everything changes. But in what sense? It’s not that the frame actually adds anything. It’s just that the frame opens the abyss of suspicion.
This is the insufficiency of our ideology. We apply our carried ideology to the next task or project or technology, and we can change them in and out like the glasses. The ideologies we apply to code can change drastically, or mutate over time as we are exposed to success and failure. In many ways then, success is as harmful (even more so) than failure because success never prompts us to question the status quo.
I’m a better developer not because I’ve been strapped to a rocket and gotten high-fives on the way up, but specifically because I’ve done so many dumb things, because I’m constantly seeking to approach the fact of being a developer with different approaches and techniques — the dogma and ideologies of the developer. In a never ending process, I have built my built my ideology from observation, failure, and recovery — what is yours built from?