IEEE Software - The Pragmatic Designer: Two Kinds of Iteration

This column was published in IEEE Software, The Pragmatic Designer column, January-February 2022, Vol 39, number 1.

ABSTRACT: There are two kinds of iteration, but they are commonly conflated. The first is the evolution of a design and its implementation to become more suitable over time: design-focused iteration (DFI). The second is the evolution of an artifact (like code) to become more suitable over time: code-focused iteration (CFI).

CFI improves only the code and ignores the refinement relationship between design and code. In contrast, a goal of DFI is nurturing and improving the refinement relationship so that the design becomes more stable and valuable over time.

Refactoring happens in both kinds of iteration. In CFI, the refactoring is shallow and textual. In DFI, the refactoring is conceptual and yields what Domain Driven Design calls “deep models” and “supple designs”.


Pre-publication draft. Please click this official link so your view counts in the IEEE's records of article views – plus the IEEE site has profesionally typeset PDFs.

In the allegory of the cave, Plato argued that invisible concepts, such as geometry, could be more true than any figures that we imperfectly scratch in the sand. The triangles and squares that we can observe with our eyes are just shadows cast on the wall of a cave by the pure ideas that we cannot observe directly. This presents us with a choice: should we fixate on the shadows we can see, or use them to discover hidden truths?

Today, two and a half millennia after Plato wrote his allegory, software developers make that same choice. Some developers see source code as the truth. Others see source code as the shadow on the wall that provides clues about the truth, which is the problem and solution that cannot be observed directly. I doubt Plato would be surprised that we are still debating.

Many developers consider themselves as pragmatic and therefore decide that seeking invisible truth is something left for philosophers and academics. I disagree. The Wright brothers were deeply pragmatic, yet in their quest to be the first to fly they both built airplanes and developed theories about aviation. They built an airplane before others precisely because they pursued both.

Importantly, the Wright brothers used an iterative approach. Iterations forced them to build something instead of spending all their time on philosophy. Iteration is what makes it possible, and indeed pragmatic, for engineers to both get things working and seek the invisible truths that explain how to make things work better.

Here’s the rub: Iteration means different things to different people. “Code is the truth”developers iterate on the code, adding features over time. I call this code-focused iteration (CFI). “Shadow on the wall”developers iterate on their understanding and on the code, making both better over time. I call this design-focused iteration (DFI).

Because the word “iteration”is ambiguous, developers can declare “we are iterating”and yet be doing quite different things. Small changes in day-to-day activities lead to different outcomes after just a few months. Developers doing code-focused iteration erode their designs, impair their readiness for the next requirement, and reduce their productivity. In contrast, developers doing design-focused iteration strengthen their design with each iteration, solve their problems better, and enjoy their work.

Kinds of iteration

What do CFI and DFI look like in practice? Let’s start with some familiar non-software examples of iteration. When you get a new pair of eyeglasses, your optometrist uses iteration to adjust them to fit your head. The two of you alternate between wearing and adjusting the glasses until both of you are satisfied with how well they fit. The optometrist is using a hill-climbing algorithm: examining the situation and making a change for the better. In this kind of iteration, no one is seeking invisible truths. You and the optometrist attend solely to what is visible, using a technique to improve a machine (your eyeglasses) for the better. This is code-focused iteration, but with eyeglasses instead of code.

Car engines are another example. When a new generation of engine comes out, it typically has unforeseen problems. The automotive engineers identify and fix these problems iteratively and, over several years, as design flaws are fixed, that generation of engine becomes more reliable. This is code-focused iteration, but with engines instead of code.

Those same automotive engineers, however, are also doing something else. Across generations of engines, they are building up their understanding of everything involved with building engines: the materials, the combustion, the wear on parts, the machines that create the engines, the environment the engines will be placed into, etc. They are using their experience with the tangible to learn about the invisible. By building up their understanding of the invisible truths, their next generation of engines will be better than the previous. This is design-focused iteration applied to engines.

Let’s return to software development. Imagine a system used to schedule university classes that already handles semesters, and let’s say the developers receive a new requirement: trimesters. The developers make minor changes to the code to support the requirement. (You can imagine many similar changes that would not force any significant reflection on the nature of university classes or on the design of the software.) Developers can make those changes by attending solely to the code itself, applying a hill-climbing algorithm. This is code-focused iteration.

Consider a different requirement for this university software: that teachers can attend classes. Let’s say the code has one data structure for teachers and another for students. If professor John Doe wants to take a class, the system would be tracking him twice, with his information duplicated in the two data structures. So, this requirement forces developers to reflect on what they understand about university classes. They iterate on their invisible understanding of how things work and revise their ideas. Perhaps they land on the idea of introducing two new concepts: people and roles. Where they previously thought of teachers and students, they now think of people who play the roles of teachers and students. They revise the code to match this new understanding. This is design-focused iteration.

Code refines a design

It’s tempting to ignore distinctions between CFI and DFI, instead thinking only of developers making a series of edits to the code to improve it. After all, developers may interleave thinking and coding, and in fact this can accelerate their design-focused iterations. But failure to distinguish CFI from DFI can doom a project. When Ward Cunningham coined the term technical debt, he described how iteration, done poorly, could bring “[e]ntire engineering organizations …to a stand-still” [1]

So, what is CFI missing? In a word, refinement. Design-focused iteration improves both the design and the code so that, over time, the design becomes an increasingly good fit to the problem at hand. In contrast, code-focused iteration, by accumulating features, improves only the code.

Refinement is the relationship between design and code. Your design guides your code and limits some of your implementation choices. Anything present in the design must also be in the code, but not vice-versa. Consider the university class scheduling system in the example above. You have a lot of implementation choices. You could implement it in any programming language, using any variety of algorithms, and on any hardware platform. However, there are limits. The ideas from the design -- people and roles, semesters and trimesters -- may not be contradicted in the code.

As a developer, why should you voluntarily constrain yourself? How can shackles help you solve problems? A good design makes it easier to write good code. In the example above, the design change from teacher-students to people-roles isn’t a shackle, it’s a gift. Clear thoughts in the design can avoid any number of corner cases in the code. A good design allows you to make broad conclusions without reading through every line of code. For example, a map-reduce design insists that each map job be idempotent, so you can conclude that it’s safe to reschedule jobs that are running slowly. The idea of idempotence is one of those invisible truths in a design that you cannot see directly in code.

Geometry is more true, and in ways more real, than any imperfect diagrams we might draw. In the same vein, a design can feel more true than source code. Consider the vending machine problem that’s often used in introductory programming courses along with a finite state machine design. Is that design not more true and real than any student’s code that implements it? And if you had a new requirement, perhaps to handle a new coin, wouldn’t you revise the state machine and then edit your code to match it?

Iteration with a goal

Years ago, when waterfall processes were common, refinement was a fact of life. Developers were forced to confront the refinement relationship between design and code because design happened early in the project and code not until later. All developers were aware of how their design related to their code.

When I mention waterfall processes, some people misinterpret this as me advocating for up-front design. The goal is to have a refinement relationship between design and code, but that goal can be accomplished through up-front design or iterative design. As Desmond D’Souza and Alan Cameron Wills said: “Refinement is a relationship, not a sequence.”[2] Plenty of articles have demonized up-front design, but the bigger problem is neglecting the goal of refinement.

Consider the two iterative processes shown in Figure 1. One will help you improve the code, while the other improves both the code and the design [3]. Teams using design-focused iteration are bringing the design with them on their journey. It is a constant companion. CFI and DFI are both iterative, but only DFI has the goal of nurturing the refinement between design and code.

Figure 1: Two kinds of iteration

Code-focused iteration Design-focused iteration
  1. Get new requirement / feature
  2. Write test case
  3. Edit code minimally so test passes
  4. Later on, refactor to remove code duplication
  1. Get new requirement / feature
  2. Revise the design, if necessary (Is the architecture OK? Is the domain model OK?)
  3. Write test case
  4. Revise code to match the design

Code-focused iteration is vulnerable to problems that grow worse over time [4]. The first problem is the sedimentary buildup of old ideas. In the university example above, you probably could have edited the code so that your teacher and student data structures survived. Obsolete ideas can accumulate in code like sediment, making it hard for other developers to understand the design and reason about it.

The second problem is loss of intellectual control. If you iterate only on the code, whatever design you have will deteriorate and provide less value. You lose your ability to reason through the system using abstraction and instead must trace the code line by line. When obsolete ideas accumulate and intellectual control is lost, projects become technical zombies without vitality.

Refactoring the design, not just the code

For decades, refactoring has been held up as the way to repair iteration’s flaws. That’s only partly right. Most projects that use refactoring merely to textually rearrange code. How do developers make that mistake? If you look at books and websites on refactoring techniques, you’ll see them describe mechanical activities, but those are shadows on the wall. Such refactoring is helpful, but it’s akin to fixing the grammar and spelling in an essay with half-baked or obsolete ideas.

The truly valuable part of refactoring is invisible. The best description of how to use refactoring to evolve your design is in the Domain Driven Design book section on “Refactoring Toward Deeper Insight”[5]. It suggests that the goal is to develop “deep models”and “supple designs” which happens during breakthroughs:

[C]ontinuous refactoring prepares the way for something less orderly. Each refinement of code and model gives developers a clearer view. This clarity creates the potential for a breakthrough of insights. A rush of change leads to a model that corresponds on a deeper level to the realities and priorities of the users. Versatility and explanatory power suddenly increase even as complexity evaporates.

That is exactly what you hope to achieve by iterating. However, 17 years after that was written, most developers are still refactoring superficially, doing code-focused iteration. If I had to guess why, I would say it’s because most developers haven’t heard of the idea, or think that the entire DDD package of ideas is a poor fit for their project and so neglect this critical technique.

Figure 2: What kind of iteration are you using?

How to recognize design-focused iteration.
  • Q: When you make a change to the code, is there often a corresponding change to the design?
    A: In DFI, you co-evolve the code and the design.
  • Q: Over time, do your design abstractions fit the problem increasingly well?
    A: In DFI, you evolve your design to avoid special cases and bent rules.
  • Q: As time goes on, is it easier or harder to build the next feature?
    A: In DFI, your abstractions are a foundation that speeds development, not a liability to be worked around.
  • Q: Do you have design abstractions that are not directly expressible in code?
    A: In DFI, developers think about and talk about design abstractions that their programming language cannot express (e.g., idempotence).
  • Q: If you had been given the requirements all at once instead of sequentially, would you have designed something like this?
    A: In DFI, you course-correct your design in each iteration. Your design and code should look like you knew what you were doing all along, even though your understanding grew gradually.
  • Q: Is the team gaining insight into the matters at hand?
    A: In DFI, the team builds up a theory of the problem and solution.
  • Q: In each iteration, do you build a revised running system?
    A: In DFI, both design and code are updated in an iteration. If you iterate on the design alone, that’s a phase in a waterfall process.

Iterate toward a clean design

Plato wrote the Allegory of the Cave to teach us that invisible ideas can be more important than the visible shadows on the wall. We read his words thousands of years later not because they are easy but because they are uncomfortable. It’s far easier and comfortable to attend to what we can see directly than to heed someone ranting about hidden truths. In fact, the second half of the allegory discusses how people who have only ever seen shadows would react when told about the invisible figures casting those shadows. Plato’s verdict was grim: they would kill the messenger.

When I look around our industry at what teams are doing, I see many good practices such as iteration, refactoring, testing, and automated deployments. Despite those similarities, some teams are succeeding and others are suffering. What distinguishes them is how well they nurture their design (see Figure 2). As teams abandon up-front design, I fear that many of them are doing code-focused iteration, accumulating technical debt through sedimentary layers of obsolete ideas, and building technical zombies.

For a long time we believed that iteration and refactoring were sufficient to keep a design healthy, but we can no longer believe that after seeing so many tangled designs and zombie projects. Software development is an intensely cognitive activity that cannot be reduced to simple activities repeated mechanically. Good design, while invisible, is critical and must be a goal of refactoring. By recognizing the distinction between code-focused and design-focused iteration, developers can adjust their activities slightly to keep their design healthy.

Pre-publication draft. Please click this official link so your view counts in the IEEE's records of article views – plus the IEEE site has profesionally typeset PDFs.

References

  1. W. Cunningham, The WyCash Portfolio Management System, in Proc. OOPSLA 92, Vancouver, Canada, Oct. 5–10, 1992.
  2. D. D’Souza and A. C. Wills, Objects, Components, and Frameworks with UML: The Catalysis Approach. Boston: Addison-Wesley, 1998.
  3. M. Keeling, T. Halloran, G. Fairbanks, Garbage Collect Your Technical Debt, IEEE Software, vol 38, no. 5. Sept.-Oct. 2021.
  4. G. Fairbanks, The Rituals of Iterations and Tests, IEEE Software, vol. 37, no. 6, pp. 105–108, Nov.–Dec.2020.
  5. E. Evans, Domain-Driven Design, Addison-Wesley, 2004.