A Theory of Programming



November 21, 2007
URL:http://www.drdobbs.com/architecture-and-design/a-theory-of-programming/204201170

Kent Beck is the author of Implementation Patterns, from which this article is adapted. Copyright (c) 2008 Pearson Education. All rights reserved.


No list of patterns, no matter how exhaustive, can cover every situation that comes up while programming. Eventually (or even frequently) you'll come upon a situation where none of the cookie cutters fits. This need for general approaches to unique problems is one reason to study the theory of programming. Another is the sense of mastery that comes of knowing both what to do and why. Conversations about programming are also more interesting when they cover both theory and practice.

Each pattern carries with it a little bit of theory. There are larger and more pervasive forces at work in programming than are covered in individual patterns, however. This section describes these cross-cutting concerns. They are divided here into two types: values and principles. The values are the universal overarching themes of programming. When I am working well, I hold dear the importance of communicating with other people, removing excess complexity from my code, and keeping my options open. These values -- communication, simplicity, and flexibility -- color every decision I make while programming.

The principles described here aren't as far-reaching or pervasive as the values, but each one is expressed by many of the patterns. The principles bridge between the values, which are universal but often difficult to apply directly, and the patterns, which are clear to apply but specific. I have found it valuable to make the principles explicit for those situations where no pattern applies, or when two mutually exclusive patterns apply equally. Faced with ambiguity, understanding the principles allows me to "make something up" that is consistent with the rest of my practice and likely to turn out well. These three elements -- values, principles, and patterns -- form a balanced expression of a style of development. The patterns describe what to do. The values provide motivation. The principles help translate motive into action. The values, principles, and patterns here are drawn from my own practice, reflection, and conversation with other programmers. We all draw from the experience of previous generations of programmers. The result is a style of development, not the style of development. Different values and different principles will lead to different styles. One of the advantages of laying out a programming style as values, principles, and practices is that it is easier to have productive conflict about programming this way. If you want to do something one way and I another, we can identify the level of our disagreement and avoid wasting time. If we disagree about principles, arguing about where curly braces belong won't solve the underlying discord.

Values

Three values that are consistent with excellence in programming are communication, simplicity, and flexibility. While these three sometimes conflict, more often they complement each other. The best programs offer many options for future extension, contain no extraneous elements, and are easy to read and understand.

Communication

Code communicates well when a reader can understand it, modify it, or use it. While programming it's tempting to think only of the computer. However, good things happen when I think of others while I program. I get cleaner code that is easier to read, it is more cost-effective, my thinking is clearer, I give myself a fresh perspective, my stress level drops, and I meet some of my social needs. Part of what drew me to programming in the first place was the opportunity to commune with something outside myself. However, I didn't want to deal with sticky, inexplicable, annoying human beings. Programming as if people didn't really exist paled after only a couple of decades. Building ever-more-elaborate sugar castles in my mind became colorless and stale.

One of the early experiences that led me to focus on communication was discovering Knuth's Literate Programming: a progam should read like a book. It should have plot, rhythm, and delightful little turns of phrase. When Ward Cunningham and I first read about literate programs, we decided to try it. We sat down with one of the cleanest pieces of code in the Smalltalk image, the ScrollController, and tried to make it into a story. Hours later we had completely rewritten the code on our way to a reasonable paper. Every time a bit of logic was a little hard to explain, it was easier to rewrite the code than explain why the code was hard to understand. The demands of communication changed our perspective on coding.

There is a sound economic basis for focusing on communication while programming. The majority of the cost of software is incurred after the software has been first deployed. Thinking about my experience of modifying code, I see that I spend much more time reading the existing code than I do writing new code. If I want to make my code cheap, therefore, I should make it easy to read.

Focusing on communication improves thinking by being more realistic. Part of the improvement comes from engaging more of my brain. When I think, "How would someone else see this?" different neurons are firing than when I'm just focused on myself and my computer. I take a step back from my isolated perspective and see my problem and solution anew. Another part of the improvement comes from the reduced stress of knowing that I am taking care of business, doing the right thing. Finally, as a socially oriented species, explicitly accounting for social issues is more realistic than working at pretending they don't exist.

Simplicity

In The Visual Display of Quantitative Information, Edward Tufte has an exercise where he takes a graph and starts erasing all the marks that don't add information. The resulting graph is novel and much easier to understand than the original.

Eliminating excess complexity enables those reading, using, and modifying programs to understand them more quickly. Some of the complexity is essential, accurately reflecting the complexity of the problem to be solved. Some of the complexity, though, represents the claw marks our fingernails make as we struggle to get the program to run at all. It is this excess complexity that removes value from software, both by making the software less likely to run correctly and more difficult to change successfully in the future. Part of programming is to look back at what you've done and separate the wheat from the chaff.

Simplicity is in the eye of the beholder. What is simple to an expert programmer, familiar with the power tools of the craft, might be overwhelmingly complex to a beginner. Just as good prose is written with an audience in mind, so good programs are written with an audience in mind. Challenging your audience a little is fine, but too much complexity will lose them.

Computing advances in waves of complexity and simplification. Mainframe architectures became more and more baroque until mini-computers came along. The mini-computer didn't solve all the problems of a mainframe, but it turned out that for many applications those problems weren't all that important. Programming languages, too, go through waves where they get more complex and then simpler. C begets C++, which begets Java, which is now becoming itself more complicated.

Pursuing simplicity enables innovation. JUnit was much simpler than the testing tools it largely replaced. JUnit spawned a variety of look-alikes, add-ons, and new programming/testing techniques. The latest release, JUnit 4, has lost that "bare metal" feel, although I made or concurred with each of the complexifying decisions. Someday someone will come up with a much simpler way for programmers to write tests than JUnit. The new idea will enable a further wave of innovation.

Apply simplicity at all levels. Format code so no code can be deleted without losing information. Design with no extraneous elements. Challenge requirements to find those that are essential. Eliminating excess complexity illuminates the remaining code, giving you a chance to approach it afresh.

Communication and simplicity often work together. The less excess complexity, the easier a system is to understand. The more you focus on communication, the easier it is to see what complexity can be discarded. Sometimes, however, I find a simplification that would make a program harder to understand. I choose communication over simplicity in these cases. Such situations are rare but usually point to some larger-scale simplification I'm not yet seeing.

Flexibility

Of the three values listed here, flexibility is the justification used for the most ineffective coding and design practices. To retrieve a constant, I've seen programs look up an environment variable containing the name of a directory containing a file in which is found the constant value. Why all the complexity? Flexibility. Programs should be flexible, but only in ways they change. If the constant never changes, all that complexity is cost without benefit.

Since most of the cost of a program will be incurred after it is first deployed, programs should be easy to change. The flexibility I imagine will be needed tomorrow, though, is likely to be not what I need when I change the code. That's why the flexibility of simplicity and extensive tests is more effective than the flexibility offered by speculative design.

Choose patterns that encourage flexibility and bring immediate benefits. For patterns with immediate costs and only deferred benefits, often patience is the best strategy. Put them back in the bag until they are needed. Then you can apply them in precisely the way they are needed.

Flexibility can come at the cost of increased complexity. For instance, user-configurable options provide flexibility but add the complexity of a configuration file and the need to take the options into account when programming. Simplicity can encourage flexibility. In the above example, if you can find a way to eliminate the configurable options without losing value, you will have a program that is easier to change later.

Enhancing the communicability of software also adds to flexibility. The more people who can quickly read, understand, and modify the code, the more options your organization has for future change. The patterns that follow encourage flexibility by helping programmers create simple, understandable applications that can be changed.

Principles

The implementation patterns aren't the way they are "just because". Each one expresses one or more of the values of communication, simplicity, and flexibility. Principles are another level of general ideas, more specific to programming than the values, that also form the foundation of the patterns.

Examining principles is valuable for several reasons. Clear principles can lead to new patterns, just as the periodic table of the elements led to the discovery of new elements. Principles can provide an explanation for the motivation behind a pattern, one connected to general rather than specific ideas. Choices about contradictory patterns are often best discussed in terms of principles rather than the specifics of the patterns involved. Finally, understanding principles provides a guide when encountering novel situations.

For example, when I encounter a new programming language I use my understanding of principles to develop an effective style of programming. I don't have to ape existing styles or, worse, cling to my style in some other programming language (you can write FORTRAN code in any language, but you shouldn't). Understanding principles gives me a chance to learn quickly and act with integrity in novel situations. What follows is the list of principles behind the implementation patterns.

Local Consequences

Structure the code so changes have local consequences. If a change here can cause a problem there, then the cost of the change rises dramatically. Code with mostly local consequences communicates effectively. It can be understood gradually without first having to assemble an understanding of the whole.

Because keeping the cost of making changes low is a primary motivation behind the implementation patterns, the principle of local consequences is part of the reasoning behind many of the patterns.

Minimize Repetition

A principle that contributes to keeping consequences local is to minimize repetition. When you have the same code in several places, if you change one copy of the code you have to decide whether or not to change all the other copies. Your change is no longer local. The more copies of the code, the more a change will cost.

Copied code is only one form of repetition. Parallel class hierarchies are also repetitive, and break the principle of local consequences. If making one conceptual change requires me to change two or more class hierarchies, then the changes have spreading consequences. Restructuring so the changes are again local would improve the code.

Duplication is not always obvious until after it has been created, and sometimes not for a while even then. Having seen it I can't always think of a good way to eliminate it. Duplication isn't evil, it just raises the cost of making changes.

One of the ways to remove duplication is to break programs up into many small pieces -- small statements, small methods, small objects, small packages. Large pieces of logic tend to duplicate parts of other large pieces of logic. This commonality is what makes patterns possible -- while there are differences between different pieces of code, there are also many similarities. Clearly communicating which parts of programs are identical, which parts are merely similar, and which parts are completely different makes programs easier to read and cheaper to modify.

Logic and Data Together

Another principle corollary to the principle of local consequences is keeping logic and data together. Put logic and the data it operates on near each other, in the same method if possible, or the same object, or at least the same package. To make a change, the logic and data are likely to have to change at the same time. If they are together, then the consequences of changing them will remain local.

It's not always obvious at first where logic or data should go to satisfy this principle. I may be writing code in A and realize I need data from B. It's only after I have the code working that I notice that it is too far from the data. Then I need to choose what to do: move the code to the data, move the data to the code, put the code and data together in a helper object, or realize I can't at the moment think of how to bring them together in a way that communicates effectively.

Symmetry

Another principle I use all the time is symmetry. Symmetries abound in programs. An add() method is accompanied by a remove() method. A group of methods all take the same parameters. All the fields in an object have the same lifetime. Identifying and clearly expressing symmetry makes code easier to read. Once readers understand one half of the symmetry, they can quickly understand the other half.

Symmetry is often discussed in spatial terms: bilateral, rotational, and so on. Symmetry in programs is seldom graphical, it is conceptual. Symmetry in code is where the same idea is expressed the same way everywhere it appears in the code.

Here's an example of code that lacks symmetry:

void process() {
   input();
   count++;
   output();
}

The second statement is more concrete than the two messages. I would rewrite this on the basis of symmetry, resulting in:

void process() {
   input();
   incrementCount();
   output();
}

Still this method violates symmetry. The input() and output() operations are named after intentions, incrementCount() after an implementation. Looking for symmetries, I think about why I am incrementing the count, perhaps resulting in:

void process() {
   input();
   tally();
   output();
}

Often, finding and expressing symmetry is a preliminary step to removing duplication. If a similar thought exists in several places in the code, making them symmetrical to each other is a good first step towards unifying them.

Declarative Expression

Another principle behind the implementation patterns is to express as much of my intention as possible declaratively. Imperative programming is powerful and flexible, but to read it requires that you follow the thread of execution. I must build a model in my head of the state of the program and the flow of control and data. For those parts of a program that are more like simple facts, without sequence or conditionals, it is easier to read code that is simply declarative.

For example, in older versions of JUnit, classes could have a static suite() method that returned a set of tests to run.

public static junit.framework.Test suite() {
   Test result= new TestSuite();
   ...complicated stuff...
   return result;
}

Now comes the simple, common question -- what tests are going to be run? In most cases, the suite() method just aggregates the tests in a bunch of classes. However, because the suite() method is general, I have to go read and understand the method if I want to be sure. JUnit 4, on the other hand, uses the principle of declarative expression to solve the same problem. Instead of a method returning a suite of tests, there is a special test runner that runs the tests in a set of classes (the common case):

@RunWith(Suite.class)
@TestClasses({
   SimpleTest.class,
   ComplicatedTest.class
})
class AllTests {
}

If I know that tests are being aggregated using this method, I only need to look at the TestClasses annotation to see what tests will be run. Because the expression of the suite is declarative I don't need to suspect any tricky exceptions. This solution gives up the power and generality of the original suite() method, but the declarative style makes the code easier to read. (The RunWith annotation provides even more flexibility for running tests than the suite() method, but that's a story for a different book.)

Rate of Change

A final principle is to put logic or data that changes at the same rate together and separate logic or data that changes at different rates. These rates of change are a form of temporal symmetry. Sometimes the rate of change principle applies to changes a programmer makes. For example, if I am writing tax software I will separate code that makes general tax calculations from code that is particular to a given year. The code changes at different rates. When I make changes the following year, I would like to be sure that the code from preceding years still works. Separating them gives me more confidence in the local consequences of my changes.

The rate of change applies to data. All the fields in a single object should change at roughly the same rate. For example, fields that are modified only during the activation of a single method should be local variables. Two fields that change together but out of sync with their neighboring fields probably belong in a helper object. If a financial instrument can have its value and currency change together, then those two fields would probably better be expressed as a helper Money object:

setAmount(int value, String currency) {
   this.value= value;
   this.currency= currency;
}

becomes:

setAmount(int value, String currency) {
   this.value= new Money(value, currency);
}

and then later:

setAmount(Money value) {
   this.value= value;
}

The rate of change principle is an application of symmetry, but temporal symmetry. In the example above, the two original fields value and currency are symmetrical. They change at the same time. However, they are not symmetrical with the other fields in the object. Expressing the symmetry by putting them in their own object communicates their relationship to readers and is likely to set up further opportunities to reduce duplication and further localize consequences later.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.