Ask HN: Is a well designed program really bad?
I currently make the experience that I have some seriously designed code base which is beautiful to read. But it's not very useful, because it basically doesn't cover any of the edge cases (and in real life there are a lot of edge cases, and even one of the low-likely edge cases can piss off a user a lot for good reasons like losing their data or wasting 20 hours of their time). So now I start updating it according to user feedback. It covers more and more edge cases and users begin to feel more comfortable in it. But it's basically impossible to keep the code clean and readable (just adding logging output alone makes it way harder to read).
So, is it really true, that you can't code beautiful and useful at the same time? Should I stop worrying about great design and start to hack my way through bug reports and feature requests? Because that's exactly what the really productive, fast paced coders in my company do.
8 comments
[ 5.3 ms ] story [ 26.8 ms ] threadI have a tool that interacts with an ssh connection by emulating a terminal (keywords: spawn, expect, python-pexpect). The general case is you start the connection, send shell commands, and expect a prompt at the end. But what can also happen is that your connection gets closed in the middle of the interaction by the server ("Connection to <IP> closed by host."), that a timeout in my code, in the expect request, or in the ssh connection terminates, or that you get error messages like "broken pipe" with huge timeouts, etc. Then it might also happen that the shell command of the user has a syntax error. While I would argue the user should simply parse the output he receives, the user says he wants my framework to throw an exception to tell him about the syntax error. Adding all these cases has bloated my code a lot. And until now I only have three users.
Yes bad things do happen in the real world: connections fail, files are missing, input are invalid, programs have bugs,...
Yes you do have to handle it: validate data, check status code, add guard conditions, report errors, document input, returned codes and thrown exceptions. Here is a nice article on error handling in programs if you are interested (for node.js but most is universal): https://www.joyent.com/developers/node/design/errors.
Yes it makes program larger. That cute 10 liners that throw an exception to the user's face if anything goes wrong will end up larger.
But does it make the code bad? No it does not. It makes the code correct. In business software, edge cases and error handling routinely take more effort than the standard case where everything goes right. It's not a wart, it's a fact of life.
A piece of code is not finished when it appear to work. It is finished when it is written, robust, clean, documented and tested. Cutting corner means that when the bug report inevitably arrives a lot of time and energy will need to be spent. Way more than doing the right thing the first time.
"that's exactly what the really productive, fast paced coders in my company do.". They appear productive and fast paced but they are not. They only appear to be so because they twist the definition of done. By moving the goalpost they throw a bunch of garbage over the wall wishing good luck for some sucker later on. But, hey, it sures look good for management. That's not even adding technical debt, technical dept implies you make a deliberate trade-off. This is just burning money.
Other than that you need a balance and since your customers pay, they have a priority so if they need a feature, you might first want to deliver something that works and then re-factor.
The general strategy[1] is to make sure what you're designing fits into an existing design pattern. It should be rare for you to have a situation where you're not within the confines of a well-known, well-understood design pattern. The sooner you become familiar with patterns, the more rapidly solutions that fit those patterns will come about in your head. The solutions will also feel less ugly, since you're essentially still being conventional.
You should be able to describe, in English, what design pattern you're using to come up with a fix.
[1] I didn't say this was a perfect strategy, but I think it helps.[2]
[2] Well, it helps me anyway. :-)
That's the same principle in physics (and really in all sciences), where from a lot of seemingly disparate and noisy experimental data, you elaborate a general theory, with a model that's beautiful and simple (ie. which reduces to a few simple and nice equations).
Some design patterns are useful to perform this abstraction, like the interpreter design pattern or the emacs design pattern.
On the other hand, it means that most of the code you will write or use, won't deal with the specific problem, but with more abstract considerations (such as how to manage resources, or how to transform code), like in physics, most of the theory don't deal with actual physical phenomenon, but are actually mathematical theories that seem quite remote.
Let's take a few examples.
Often, users specify more cases than needed. For example, they may say that some price depends on some parameter, such as the number of persons:
It is obvious that there's actually a formula: price = 10*2^(nperson-1)).Actually, there's always a simple formula, since any set of N points can be extrapolated by a polynom of degree N+1.
The interpreter pattern let you decompose the operations you have to perform in the different cases, in a set of simple operations that are specific to the problem domain. The specific cases can then expressed as simplier "programs" using those operations. In a way, this allows to decompose the problem in two orthogonal parts, one set that contains generic simple operations, and another that contains simplier programs specific to the concrete cases. Since those real-life cases often change, having a domain specific language to express them let also write them more easily and quickly, and even dynamically (ie. change the specific case programs without changing the software, just changing the data that is intepreted).
Another pattern found in emacs, and similar to the garbage collector, is the display engine.
In the case of the garbage collector, we decouple the memory management from the actual program, by having a separate algorithm, orthogonal to the problem specific algorithms, to deal with the problem of memory allocation and release. Once the garbage collector has all the information it needs to be able to release safely the memory that is not used anymore, it can do its job without interfering with the domain specific program.
Similarly, the display engine is entirely decoupled from the rest of the editor in emacs. The display engines is able to detect by itself when the contents of the buffers change, and to compute alone the difference between what is displayed on the screen and what needs to be displayed after the changes. It can then produce an optimized update sequence for the screen or terminal. The rest of the emacs editor routines can modify the buffers with absolutely no consideration for the displaying, which simplifies greatly their code.
In conclusion, if write your program as some general rule performing the same treatment to all cases, and encode the specific real-life cases as specific data to be processed by the general rule, you can obtain a program that still handle all the specific cases, but doing that in the most general way, and therefore being as clean and as well designed as you wish.
This is a natural process by which software grows and the developer gains new understanding of their own product. I would suggest going ahead and making whatever ugly changes you need now, and then refactor back into an orderly structure as things settle down. You'll probably have made new insights by then and will be able to update the design to match.