Well Factored Programs Cannot Be Understood Statically

I have read here and elsewhere that XP practices reinforce themselves. I think it intuitively makes sense that a suite of UnitTests and AcceptanceTests reinforce the act of RefactorMercilessly. I propose that these tests also support understanding the resulting well-factored code (or mess depending on how long it's been since I've last looked at it) by putting it into a context.

I think there are at least two dimensions to the context. One dimension is understanding the point of the code and why it is as it is. Even with VeryLongDescriptiveNamesThatProgrammingPairsThinkProvideGoodDescriptions, it's hard to know why a piece of code exists in the first place. Is it an artifact of refactoring (such as a Visitor, a State, or a callback for DoubleDispatch)? Or, is it a piece of the domain? It is a partial pattern because the full one was too much?

moved to WhatItTakesToGrokCode

The second dimension is about importance. Not every piece of code is as important as the other for the purposes of understanding what's going on (of course, every piece of executed code is equally important for the correct (or incorrect) functioning of a program). When I read code for the first time, or even code I wrote that has grown stale, I find that I unconsciously treat every piece of code with the same level of importance. This brings on the SevenPlusOrMinusTwo problem because I don't know what to flush from my cache.

I have only recently discovered that the solution to this problem is reading the code dynamically through a test case. Even though I think I'm a reasonably competent programmer (although, this may be a UsefulLie I tell myself), there's nothing like watching the debugger go through code to quickly generate a context in my head. If I understand what the test case is trying to accomplish, then I can see how all pieces fit together to accomplish that goal. Furthermore, all the important stuff becomes immediately obvious and the unimportant stuff falls away.

Provided you picked an important testcase to start with, neh?

Good point. It's generally easy for me to map a test to pieces of code, but I think it would useful to have a reverse mapping: Code to test cases. UberReflectiveEnvironments should be able to run a series of tests and generate a map of all the code each test runs.


I think that to some extent there might be a difference between "taming" code - as in getting it to do a new trick just for you as a way of establishing your mastery of it - and "understanding" code - as in getting a comprehensive enough picture of it that you can see how the various parts of the design balance each other, where you might look for problems to solve, etc. Starting with the actual code or with the tests might not be relevant to that difference.

It has been suggested to me that "long methods" might be easier to understand, as they might not "hide the forest" as much, or preserve more of the so to speak "narrative" patterns of code. I can see the sense in that observation. But I also have an intuition of how the drive to reduce complexity - to remove any method, any class, any bit of code that can be removed without lowering the quality of the design - together with flair in naming - might yield an (nearly) instantly understandable whole.

WellFactoredCode should generally be easier to tame, but LotsOfShortMethods might make it slightly harder to understand until more subtle refactorings are applied, those which restore the "literary" cohesion of the code - the stuff of which patterns are made. Therein, possibly, lies the difference between WellFactoredCode and code in ExtremeNormalForm .

Most of us, though, might be content with having to tame code for a while as a prelude to growing into an understanding of it over time.

Unfactored programs also cannot be easily understood. In most cases, however, a well factored program is easier to understand than its unfactored counterpart. Increasing the "understand-ability" of the program is the whole point of refactoring.

That is all true, but not to the point. The point is that the best way to understand WellFactoredCode is by watching the code perform rather than looking at it on the screen or on paper.

That might be a vicarious kind of experience. There might be a more experimental procedure - I assume this bit of code does X when I call Y on it, let's write that call and see whether that's right. Does that happen ?

It seems to me that the "debugger" argument says that stepping through code in the debugger brings about understanding because it gives you an idea of which bits of code are related to one another. Does that ring true at all ?

If so, two observations might apply - first, that "systems of names" (as opposed to very long and descriptive names) and other strategies of organization (packages, etc.) could achieve part of that effect statically.

Second, that poorly-factored code would actually detract from benefits the debugger might bring in understanding code - if you are stepping through a large method, the "context" stays stable for a long time even as the kinds of things the code is doing change.

Does your experience confirm (or invalidate) any of this ?

Increasing the "understand-ability" of the program is the whole point of refactoring.

This is not the main reason I refactor; it is the secondary reason I refactor. First, I refactor to make my coding go faster in the long run (over months and years). Second, if I can do so without sacrificing coding speed, I refactor for understandability. I usually have to sacrifice some ReverseEngineeringUnderstandability? to get maximum coding speed. -- StanSilver

What else slows you down besides not being able to understand the code?

Hard to change code slows me down more than hard to understand code. I would rather have easy to change but hard to understand code, than easy to understand but hard to change code.

An example is when you want to subclass a class, and change one line of code, and that line of code is in the middle of a method that is 30 lines long. Very easy to understand - you want to replace this one line of code in this long method in the subclass. Hard to do. You either have to refactor first, and then change it, or copy the whole method into the subclass, causing duplication and bugs later on. Pain in the butt. Especially if the superclass is a library class, or "owned" by someone else.

I know it is just semantics, but in my book, easy to change and easy to understand are not the same thing. Or, to put it another way, the code that StanSilver finds easiest to understand is not well written code. In well written code, the "spec" is an emergent property of a zillion classes and methods, and I find emergent behavior hard to reverse engineer. --srs

How about bad design ? Stuff like the following -

 String getAllowedStates() {
   return "open,closed";


String x = z.getAllowedStates(); Vector v = parseIntoVector(x);

Arguably this is understandable. But certainly you'd want to refactor it, wouldn't you ?

I would want to refactor it. It is understandable, but if it were simpler it would be even more understandable.

How would you refactor this this to be simpler?

Why not:

  Vector allowedStates = new Vector();

Or you could use the EnumeratedTypesInJava idiom:

  public class AllowedState? {
     private String name;
     private AllowedState?(String name) { this.name = name; }
     public String toString() { return name; }

public static AllowedState? OPEN = new AllowedState?("open"); public static AllowedState? CLOSED = new AllowedState?("close"); }

The page title seems to imply that it's just well-factored programs that can't be understood statically. Why would this be the case?

The scope of this page is well factored programs. I make no claims about programs that aren't well factored. I suspect that large non-factored or poorly factored programs cannot be understood at all for some definition of 'large' ;-)

Also, "statically" is a little vague: is reading the unit tests for a class (but not executing them) static? For my classes, all it usually takes for me to restore my understanding of what they do is to look at both the production code and the unit test code. The only thing that's not clear to me is whether I should be agreeing or disagreeing with this page... --GeorgePaci

I see your point. When trying to understand a system from scratch, redundancy aids understanding. For instance, concept A is introduced as context for concept C, then again as context for concept B, and again for its own sake. This redundancy "flattens" a multidimensional system so that individual views are simple and well focused. The counterpart of redundancy is partiality. Concept A will not be fully exposed when it is being presented as context for concept B.

Another way of saying this. Ever read a manual that refuses to describe something more than once? It's rife with links you have to follow and then return from. And if you "blow" a return, the consequences to your understanding are undefined.

OnceAndOnlyOnce seeks to drive redundancy to an extreme low limit, which will drive the dimensionality of the product to an extreme high limit. This solves the "did I fix all instances of X?" problem at the expense of some accessability. The debugger is offering you a linear view of a multidimensional thing, appealing to your innate sense of linearity.

Maybe the problem (if there is one) is in the decision to treat the product and its specification as the same thing (TheSourceCodeIsTheSpecification). Products should be compact and efficient. Specifications should be understandable. It would be nice to specify a system as a series of flat views, each one incomplete around the edges, and have a development tool compose a compact machine from that description, resolving the partiality and redundancy at the seams. This is the "parallel overlapping views" concept put forth by MichaelJackson and son Daniel.

-- WaldenMathews

"Redundancy aids understanding" reminds me of lessons in presenting information. First, tell the audience what you are going to tell them. Then, tell them. Finally, tell them what you've told them. How much of our difficulty in writing understandable/maintainable code stems from our inability to write? --DanilSuits

I disagree that this is an issue of writing in the sense of telling a narrative. Most well written narratives are linear (even well written, non-linear narratives have linear components and most can be put back together in a linear form). Authors, movie directors, playwrites, etc seek to make their non-linear material understandable to an audience. OnceAndOnlyOnce does not and that's ok because well written code need not be a linear thing. I agree with WaldenMathews that the debugger's chief role (coupled with good tests) in WellFactoredCode is transforming highly factored, multidimensional code into a linear form easily understood by human beings.-- MarkAddleman

The scope of this page is well factored programs. I make no claims about programs that aren't well factored.

I am not sure this is a fair argument. Any program can be understood, given enough time and effort. You must compare the ease of understanding of a well factored program with that of a non-well factored program to have any meaningful discussion. -- WayneMack

True, a factored program, especially one that was factored for understanding rather than performance, can be understood better than a chaotic one. But that's not the comparison of interest here. I think the author's point is that merciless refactoring takes the program through and beyond the best forms for human understandability, and moreover, that this happens largely outside the awareness of the refactorers (until they read the same code six weeks later). Thus, the discussion of somewhat refactored versus extremely refactored is quite a meaningful one for me. -- WaldenMathews

A point of terminology might also be in order; refactoring is formally defined to mean "improving the design of existing code (without affecting its functionality)", or at least that's what MartinFowler says it means. Thus the question at hand is whether improving the design of existing code can make that code harder to understand. To answer in the affirmative, don't we have to hold that "good design" and "understandability" are distinct and in fact orthogonal ? If so, does that tell us something about our concepts of "good design" and "understandability" ? -- LaurentBossavit

For "good design" and "understandability" to exist in a tradeoff relationship, they don't need to be distinct: they just need not to be identical. And certainly you don't mean "orthogonal", because if they were that, then you could improve design as much as you want without affecting understandability, and vice versa. Although both terms will resist crisp definition, there is definitely a OnceAndOnlyOnce "good design" dimension that is not identical with good story-telling "understandability". See OnceAndOnlyOnce for more discussion on this very topic. -- WaldenMathews

I don't think you can use "design" as a stand alone description. I can understand how "design for understandability" may oppose "design for performance," but I cannot compare "design" and "understandability." What characteristics of design seem to oppose understandability? -- WayneMack

Precisely. Where OnceAndOnlyOnce is a kind of performance. -- WaldenMathews

Pehaps design for code that is easy and fast to work with once you come up to speed/understand/grok it VS design for code that is easy to come up to speed with/understand/grok ???

Perhaps like the difference between a reference book and a tutorial ??? You don't use a reference book for the textbook in your first class, and you don't grab a tutorial when you are one year into a project and want to look up something.

To me the most important thing about system understanding is to expand the notion of system to include the participants that go home at night to see their spouses and children. I know that I have seen some well-factored systems which are hard to understand because of the degree of object decomposition, but having someone there to tell you the story of the coarse architecture does help put the all of the objects in a frame of understanding. -- MichaelFeathers

See Also: StudyTheSourceWithaDebugger, WisdomBegins, RefactoringMercilesslyHidesTheForest

View edit of September 9, 2005 or FindPage with title or text search