To See the World in a Line of Code
Mik Seljamaa 🇪🇪
Posted on March 17, 2020
We've described the central problem of software design as finding the optimal mapping between the problem and system semantics. We've also discussed the activity usually referred to as `writing documents' as doing the necessary work and capturing the results in a document. So what is involved in doing the work that does not show up in the document? It will have a lot to do with finding the optimal mapping.
The fact is, no-one ever picks up a job, sits down and rolls out the best solution as if they were doing some sort of exam questions. The designer of an effective solution will always look at the problem from several different directions, and will usually see several variations of possible solutions. The solutions must be challenged to ensure that they meet all the requirements and that they are going to be practical to implement. Only the winner will be recorded in the document. Sadly, the usual convention is to omit the details of why the documented solution was chosen over other alternatives from the document.
This point is particularly important when our dominant approach, usually the one that provides the basic structure of our process, involves top-down design. The idea of top-down is that it enables us to see the wood for the trees. In the early stages, we can see the overall intent of the system. We can then concentrate on getting the details within each subsystem right, knowing that its general direction is correct. This is distinct from the approach of doing top-down design to remain independent of the design details of the lower levels, although the two motivations are often found together.
In both cases, the design will actually have to be implemented so the designer will have to convince him or her self that the design is actually implementable. If the objective is seeing the wood for the trees, there will probably be an idea around of what the target language, operating system, or in management problems the team, actually is. A criterion for a successful design is then usually optimizing the use of system resources. If the objective is independence, the criterion is to produce a design that is implementable in all of the possible targets. Ideally, this is done by using a model explicitly common to all the targets.
This means that the designer must have considered implementation during design, even though usual practice is to lose the implementation considerations that caused the designer to prefer one design over another.
While thinking about design, it is quite common for designers to see in their minds a high-level description of the outer parts of their system, perhaps the I/O, a more detailed description of the inner parts, perhaps a group of database table definitions, and right in the middle, at the point where the key processing of the system is done, they often know just what the critical line of code, which may be quite complex, actually says. From this line, they can convince themselves that the details of the outer parts of the system will be OK without having to think them all through. It's not always at the core of a design that the ticklish bits exist - the designer might notice a critical part of a low-level error recovery protocol, and feel the need to know that it can be implemented. There is no better way to feel secure with what your design calls for, other than to be able to state at least one practical way to do it.
We are not saying that it is imperative to see lines of code popping into your head during design. We are saying that it can be a very useful way to clarify your thinking about an area, and if your thoughts do turn to code, follow them. Don't cut off these considerations because your deliverable is a higher level document. That way, you get a design document that is effective in use, and people will call you a demon wizard of the design process. Remember holding your toothbrush with chopsticks? People that are into the habit will rather believe you have a really good chopstick technique than that you just grasped the toothbrush with your fist.
Another area where little code fragments are really useful during high-level design is in getting a real sense of the system semantics that you are going to be using. We always have to learn new APIs, to our OSs, GUIs, libraries and so on. It takes years to become really fluent in all the ways we can properly use an API. So look in the books that discuss an API, and write little demo apps that demonstrate the features you think you'll be needing. This really helps concentrate your mind on what you need to keep track of from the bottom up, as your design progresses from the top down, and ensures that you don't attempt to use semantics that actually isn't there. It can be very embarrassing to produce a design that requires a different operating system design, but if you've spent a few minutes writing a little program that exercises a feature, you'll use it as it is, and never mind what the documentation claims. You win the minutes back during implementation because you can copy bits of your doodles into your source, and hack them.
Spend a while looking at the design of the APIs you use. Look at their currencies - the values passed in and out of the API. How do the bits of the interface fit together? Are they well designed? What are the idioms that their designer intended for you to use? APIs are usually done by experienced designers, and they are like little messages from very bright people about how they see the world. The style of Ken Thompson's UNIX API has survived very well for nearly 30 years. He himself said of it that the only change he would make is `I'd spell creat() with an e!'. There is something very close to the way computers work in the structure of the UNIX API
This section is all about the importance of being able to see one level below where one is working. This is true even though hiding the details of implementation is a permanent goal of our discipline. The better we get at this, the more we win, but we just aren't good enough at it yet to forget about the lower levels. Understanding where a compiler allocates heap and stack space enables you to handle scribble bugs, where we break the model of the language. Having a sense of how much physical memory (and swap) we have enabled us to write programs that will work in real-world situations. Even true virtual machines, such as the Java virtual machine, give services so low level that we can trust the implementer to do it sensibly so we can predict the efficiency of our operations.
Posted on March 17, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.