We were in the pub when Charles posed an NP-hard problem: "How do you teach programmers style and elegance in code? I would like five points or properties that I can teach to my programmers." Trusty pints of Guinness in hand, Charles, Frank and I set about trying to find some kind of answer.
Five considerations. Four Green Fields. Three conference delegates. Two days into the conference. One pint of Guinness each. No partridges or pear trees in sight.
A recent discussion on programming style and effectiveness reminded me of an evening in Tampa, Florida, during OOPSLA 2001. Frank Buschmann, Charles Weir and I decided to forgo the pre-digested delights that Tampa had to offer us, seeking refuge, beer and conversation instead in an Irish pub recommended to us by another member of the patterns community, Alan O'Callaghan.
So what do three members of the patterns community talk about over beer? The usual suspects would be patterns, the conference, something technical, something non-technical or, one of the traditional pub pastimes, solving the world's problems. It turned out to be something technical. Charles posed an NP-hard problem: "How do you teach programmers style and elegance in code? I would like five points or properties that I can teach to my programmers."
Not a trivial question to answer, but thanks to the buzz of the conference as well as the beer it was a cause and challenge we felt we could rally round. A good hour, more beer, many doodles and much discussion later we arrived at the following:
Why is symmetry important? It is a simplifying assumption. It reduces the amount of information in a design by making it more regular. But surely some asymmetry has a pleasing effect? Of course, but if you do not have a backdrop of symmetry against which you can hang asymmetry, you have just have chaos. If you're going to paint, you need a canvas.
For example, symmetry suggests balancing any resource acquisition with a release, such as having a disposal method pair with a factory method. Java programmers may assume that garbage collection will take of everything for them. That is only true (up to a point) for memory. But it is not memory you got from the factory: you got a smart, managed resource object whose lifecycle details are encapsulated. When you're done, give it back, by allowing disposal either on the resource itself or through the factory. C++ programmers may presume that using delete will do everything for them. But who said that the object was created using new? That is the point of encapsulating object creation in a factory: encapsulation. And who says actually destroying the object is the right course of action for disposal? Encapsulation begets encapsulation. Assuming a symmetric design until proven otherwise can make a factory micro-architecture looser and simpler.
Spacing refers to boundaries, separations, connections, etc. Spacing applies at the smallest level, e.g. indentation of source, as well as the largest, e.g. how interfaces are used to partition component "neighbourhoods" in a large system.
You want to make sure that your code is formatted with reasonable spacing, so that it does not look like line noise, but at the same time you don't want it drowning in space, so that readers should not find themselves scrolling up and down the screen to read what is in truth a blank screen punctuated with the occasional piece of code. When considering the big picture, you don't want hundreds of classes all cohabiting in the same space, undifferentiated and intertwined. They should be separated out into packages with the highest cohesion and lowest coupling that is reasonable.
Or, put another way, intentionality. Software is effectively invisible, and software development is in part the art and craft of shaping the invisible. However, although mechanism transparency and information hiding are useful concepts, each concept must be visible at some level to be easy to understand. Outside a class you want representation decisions to be as invisible as possible; inside it you need for them to be visible so that you can understand them and the effects of changing them.
So, making things visible should be part of the disclosure of a well-written piece of code. This is not a matter of spelling things out on drool-proof paper, which goes against economy and emergence, but of adopting techniques that make the relationships between structures and rationale as clear as possible at the right level.
For example, adopting suitable idioms can make the intent of a piece of code more visible, whether it is choosing a more specific class instead of a plain int to express whole-value concepts or using a named constant to factor out a magic number. The same principle applies all the way up to the macro-architectural scale, so the explicit interfaces between components and choice of patterns used should make the intent and interconnections more visible.
Simple rules or mechanisms can lead to profound and complex behaviour. In the real world we see this in the flocking of birds: (1) steer towards the average position of your neighbours; (2) adjust speed to match your neighbours; (3) don't bump into anything.
In software, not all the behaviour desired of a system has to be painstakingly and explicitly defined. Often behaviour arises out of the collaboration of parts, and the relationships between them, rather than the parts themselves. Many decisions do not need to be documented explicitly in code: polymorphism, function pointers, dynamic loading, etc, all offer reasonable alternatives to patchworks of special cases brought together in rambling if or switch statements. Emergence is not the same as magic there should be nothing mystical in your code but it may sometimes be in tension with the notion of visibility.
The Morning After
While I would not claim that these points, properties or principles are in any way final and definitive (recall their humble origins!), or indeed even meet Charles's challenge, I have found that they have provided a useful focus for discussion. They are qualities I have observed in much code that I would consider elegant.
However, the slippery and diverse nature of aesthetics pretty much precludes them from being thought of as rules ("Thy code shall exhibit the qualities of economy, symmetry, spacing, visibility and emergence, otherwise DLL-fire and damnation shall befall it!"). They are too abstract for such concrete causal dogma. They are also not specific enough or constructive enough to be seen as recommendations. A recommendation differs from a rule by being more of a guideline than a law of the code.
The five points are probably best classified as considerations. A consideration is a suggestion to take something on board and keep it in mind; it may (or may not) inform your design thinking rather than regimenting it. So that piece of code you were going to work on this morning? There's five possible things to consider after breakfast.
Where I am having difficulty here is that 'simplest' has no metric. So "the fewest programming environment operations to get the next test case running" could actually be what is meant. Or, "fewest lines of source code". Or, "source code using the what-I-consider simplest types of operations".
So, what is the metric for "economy" (and how would it not conflict with the other criteria)?
"Economy of what? Effort? Lines of code? CPU use?", well, yes! In its most general form, the concept of economy relates to the careful management of available resources, and any related consequences. For a good number of systems the micro and macro development practices adopted waste effort, mushroom lines of code and squander CPU.
As for not conflicting with the other considerations, they are not mutually exclusive, so I would never consider that an absolute requirement. At times they may be complementary and at others they may be in tension: that's a healthy part of what makes design design as opposed to a linear, automatic process. Of course, if they are always downright contradictory, that would be a problem!
The Economy and Emergence point really hit home. There's another way of saying this, which is to use all of the features of the underlying language rather than inventing extra machinery each time. If you see code which could be refactored to take advantage of a language feature (e.g. inheritance, method overloading, static code, even standard libraries) with less code then its worth doing.
I've noticed this a bunch of times while looking at JBoss code - understanding how a subsystem works is often harder from simply reading the text but once you see how it uses language features to get the functionality you appreciate the simplicity and flexibility of what's there.
Many coders don't use the full power of the language and don't udnerstand that a huge amoutn of thinking and careful design often goes into the underlying platform. Even after 7 years of Java I still pick up the Java Programming Language to check things out.
I believe what is needed is highly visible examples of well written systems to help everyone learn by example.
Students of any other craft/art/science have a wealth of highly regarded examples which help guide their development. I don't believe we have the same level of highly regarded examples in software that are as visible to the student.
We need a 'World Gallery of Software' that has examples of good systems (and bad?), critiqued by experts with debate etc about how it might be improved open.
I couldnt agree more with all of Kevlins five points but how do you explain any of this to someone to whom economy isnt nuff said?
Maybe teaching the habit of reading a few pages of excellent code every day wouldnt be a bad idea if you could find a body of code everyone agrees is elegant and effective. But given how little good reading Shakespeare has done my prose, I wouldnt bet on coding style by osmosis.
Of these, if you had to pick one, I'd say coupling is it. And I'd say that comprehending the coupling of any non-trivial system (and even some trivial systems) is impossible without some visualization tool. So just telling people to limit coupling to the necessary amount is like telling starving people to go eat something -- not much help.
If I had to pick two, the second would be you don't want hundreds of classes all cohabiting in the same space, undifferentiated and intertwined.
(Yeah, I realized I split #4 Visibility into two parts.)
One area where most programmers do follow the creed of Economy is in program documentation and comments in source code -- and that doesn't seem to do anybody any good.
I would substitute Clarity for Economy.
Far too many people write code that's as terse and cryptic as possible. And then there are those who heap layer upon layer of abstraction, with the idea that it improves the organization of the program in some way. Neither helps the goals of clarity, maintainability or ease of program comprehension.
The DRY principle (don't repeat yourself) is very useful. It covers awful avoidance in everything from "magic numbers" in code to broken encapsulation.
Another tenet, suggestion, paradigm, idea, suggestion, etc. is loose coupling or othogonality. Components should be able to function in maximum ignorance of each other. This is a little beyond what some people think of encapsulation. They may actually put things in classes, but have a class hierarchy of classes with a large number of different constructors parameter lists, so that client code has to have a lot of special knowlege, which could be insulated from the client by using a factory.
Development process is what should be substituted here. It is virtually impossible to guarantee any level of economy without a planned and disciplined approach. Whether it is your own personal development process or within the context of a group methodology. I believe what you meant was that you do what is needed to attain the level of quality required, balanced with the available resources and provided time, and no more.
The study of design should be accompanied by the study of designs. Visible examples are used in other professions as more than just "case studies" (a term that inspires most people to yawn and reach for their coffee, or race to the nearest exit): the visible examples form the learning medium and history of many subjects. Whether it is architecture or astronomy, their is a sense of culture and depth associated with acquiring knowledge about these domains.
In much software development I detect only an absence of this depth: that history doesn't matter; that reflection is for other people; that culture is something to do with yogurt. Open Source code, the ever-growing body of in-house legacy code, and both web and treeware publications provide us with a wealth of examples and counterexamples to reflect upon and establish a more visible culture of design in software. Some people learn better by principle, and see these exercised in examples, some people learn better by example, and see the principles as a result. But whichever perspective you take, examples make the abstract concrete.