I've been programming professionally for about nine years now, and a few years ago I got to the point where designing software is largely nonverbal. That is, I reached the fourth level of competence.
Recently I tried to explain to someone without much experience how I choose between design tradeoffs, and I found myself becoming very vague and metaphorical. For example, to me problems feel like they have a three-dimensional shape, and I try to come up with solutions that match that shape, or that change it into a simpler shape, or that serve as a bridge between two shapes. Good solutions feel "smooth" to me, and when I spend time perfecting a design, I literally feel like I'm sanding down sharp edges.
Unfortunately, that metaphor tells you virtually nothing about how I actually create software designs. If I try to explain to someone with less experience how to recognize when software is "smooth", they'll just stare at me like I'm speaking Japanese. Which is pretty much exactly what I'm doing, in terms of information transfer.
So I've been thinking more about what I actually do when I design software, and I remembered that I used to have a relatively conscious "cost function" that I used to evaluate tradeoffs. The cost function I use has become so ingrained that I don't even consciously think about applying it anymore, but it still guides (perhaps even dictates) my designs. It has just migrated from the verbal part of my brain to the tactile part, like riding a bike.
Here's an approximation of my cost function. I no longer remember what actual numbers I used back when I was figuring it out, so I reconstructed it by weighing tradeoffs in my head, and trying to come up with a sense of how much I prefer one to the other.
Disclaimer: the above is just an approximation to my internal sense of smoothness. My actual criteria involve quite a few more cost criteria, and some of the costs listed above can be more or less costly depending on the situation.
I think the idea of a cost function helps me articulate why experience is good when it comes to software design:
Over time you develop a better sense of what your cost function should be. For example, multi-assigned local variables were not part of my initial cost function until I learned how difficult it can be to maintain code that uses them. There are quite a few stubbed toes embodied in the cost function I use.
Over time your forsight gets better. It's easier to anticipate the impact of certain design decisions on the rest of your code; most importantly, code you haven't written yet. With experience you develop a better feel for what the client side of a given interface will have to look like, and that affects the API you present, because the cost of the client code is included in the total cost of the program.
Lastly, over time you develop more and more techniques for solving problems. Each technique has its own cost profile, and you have to have experience using that technique to know what its cost profile is, and how it will affect the cost profiles of the rest of the program.
Posted on November 24, 2003 08:28 PM
More programming articles
Cool.
I'm just between concious incompetence and concious competence as far as design goes, moreso on the competent side. I can come up with good designs most of the time, but I easily get stuck and have to step back and rehash.
Thanks for your cost function. One of those you mentioned that I'm just learning in my most recent project is 'parameter to constructor'. Yeah, those constructors are getting pretty messy...
It's nice to find out how an experienced software designer looks at problems.
Posted by: Luke Palmer at November 25, 2003 10:00 AMThat's a very, very interesting way of looking at things, especially so because it reminds me very much of the weighting system apparently used in the TeX hyphenation algorithm. Also, the FourLevelsOfCompetence entry at the c2 wiki further tied back to a rather entertaining paper I read a few weeks back entitled Intelligence & Changes in Regional Cerebral Glucose Metabolic Rate Following Learning, mainly because it used Tetris to demonstrate how brain activity follows the same curve as described by Alistair Cockburn over at c2: starts off low, peaks during the learning process, and drops low again in the "mastery" phase. But I digress.
How language-dependent is this cost function? Do you have different versions that apply to different languages? I'm specifically interested because you've described a generic cost framework which abstracts over syntax, but which seems to encourage a particular style of coding.
I'm sure this is because I've had my head buried in a few odd paradigms for the past couple of months, but take Forth as an example. In Good Forth Style(TM)... local variable definitions are strongly discouraged, far more so than function definitions; the weightings you give for long definitions would be greatly hiked, since functions longer than four or five lines are considered awkward at best (though I've seen some frightening counter-examples); and so on. Other languages with unusual semantics - the APL family, the Lisp family, Joy, Prolog, Smalltalk - could eventually produce a very different weighting scheme.
Hmm... I had a point, but I seem to have misplaced it. Anyhow, I guess the question I've been circumnavigating is: how much of this weighting describes how you _code_, and how much describes how _you_ code?
Posted by: Gnomon at November 26, 2003 01:26 PMMy cost function is somewhat language dependent, but probably not as much as it should be.
For example, in garbage-collected languages, transfer of memory ownership has no cost. On the other hand, shared memory ownership (as opposed to transferred ownership) is basically equivalent to a global variable, and is treated as such.
In languages without higher order functions, simulating them can be so costly that I usually avoid it -- it usually incurs a hit on "introducing a pattern", "lines of code", "transfer of memory ownership", and "class definition", to name just the most obvious penalties. In contrast, if the language supports HOFs intrinsically, there is no "introduction of a pattern" cost because the language itself introduced the pattern.
Despite that, I once wrote a 15,000 line Java program that basically simulated a language with lazy evaluation and higher-order functions. It was ten times as verbose as it would have been in haskell, but the program would have been even bigger if I had used a more conventional architecture. Also, I only had to pay the "introduction of a pattern" cost once, and then could amortize it over 30 little classes that each simulated a single lazy list function.
I haven't used Forth extensively, and it usually takes me a couple months just to solidify the most obvious parts of a cost function (the first order of magnitude, if you will). So I really don't know what a Forth-trained cost function would look like.
Oh, I forgot to mention an important cost:
In other words, try to encode each decision in one and only one place.
Posted by: kim at November 26, 2003 05:21 PMYou really need to find a copy of Thinking Forth by Leo Brodie. That has been one of only two books (the other being Writing Solid Code) that has greatly influenced the way I write code, even though I've never written any significant program in Forth.
Generally speaking though, my cost functions are not really tied to any particular language. I try to avoid having more than four or five parameters to any given function, avoid double testing of conditions, a function will only do one thing, extensive use of pre and post conditions (assert() in C) and avoidance of global variables (or at least make them read-only). I'm not so concerned about function length, but I am about number of parameters—five or more and something is wrong.
I also write with the maintanance programmer in mind (which usually ends up being me).
As an example of code design, I once wrote routines to manipulate the text screen on a PC (back when you had text mode—80x25). Most UI code back then would have a single function:
scroll_screen(screen *pscr,int x,int y);
with which to screen the text screen. A positive Y value would scroll up, negative down. Positive X would go left, and negative right … or was it the other way around? I could never remember, and just how often would one scroll vertically and horizontally?
So, with that in mind, I wrote the following API:
scroll_up(screen *pscr,unsigned x);
scroll_down(screen *pscr,unsigned x);
scroll_left(screen *pscr,unsigned x);
scroll_right(screen *pscr,unsigned x);
With the precondition that pscr would always point to a valid screen, and that 1 <= x < screen.height. Design decision on my part—scrolling of 0 doesn't make sense (at least to me it didn't) nor setting the scroll value more than the screen dimensions (since I had another function to clear the screen). Splitting the scrolling into four functions, with those restrictions made writing and testing of the code much easier, and made the rest of the code calling it much clearer in intent.
For me, this breaking up of functionality is critical, and I don't mind if a class has twenty methods, as long as each method manipulates the object in one distinct, well defined way. For the text IO object (as it could be considered) I had 43 methods defined, but each method did one well defined thing and the method names were consistent; I wasn't too concerned about there being too many methods.
Posted by: Sean Conner at November 28, 2003 12:38 AM> Unfortunately, that metaphor tells you virtually nothing about how I actually create software designs.
Yes, that's the classical philosophical distinction between the context of discovery and the context of justification.
Posted by: Andre at December 5, 2003 11:47 PMHere are some costs that it took me a long time to discover. Unlike the others, I didn't figure these out on my own; I had to read about agile development and give unit testing a try before I even realized what I was missing.
nontrivial function without unit test: +6 per linePosted by: Kim at July 16, 2006 08:50 AM
if statement without tests for both branches: +30 (over and above the +6 per line)