

Ask HN: Is a well designed program really bad? - erikb

I currently make the experience that I have some seriously designed code base which is beautiful to read. But it&#x27;s not very useful, because it basically doesn&#x27;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&#x27;s basically impossible to keep the code clean and readable (just adding logging output alone makes it way harder to read).<p>So, is it really true, that you can&#x27;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&#x27;s exactly what the really productive, fast paced coders in my company do.
======
informatimago
Covering real life in a program shouldn't make it uglier than a program
covering only textbook cases. To handle the more numerous and complex cases,
you will _generalize_ the code. This will let you avoid handling specific
cases in specialized code, but instead cleanly handle all the cases with a
single rule.

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:

    
    
        1 -> $10   2^0 
        2 -> $20   2^1
        3 -> $40   2^2
        4 -> $80   2^3
    

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.

~~~
erikb
Thanks a lot for your in depth explanation and the examples. Gives me a lot of
motivation to keep working on keeping my codebase clean.

------
Monkeyget
I take it that by design you mean code quality and that you include error
handling in edge cases.

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](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.

------
canterburry
This is where refactoring comes into play. Your beautiful design represents
your understanding of the requirements at the time which you solved. These new
requests are showing you aspects of your system you maybe didn't think of and
therefore didn't factor into your initial design. I presume had you been aware
of them all, your design would already account for them.

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.

------
pagantomato
I know exactly what you're referring to.

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. :-)

------
atmosx
I'm not even close to other experienced programmers here, but from my small
experience usually the two things play really nice together: beautiful and
useful.

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.

------
sharemywin
do you have an example of what your talking about. I'm thinking even ugly
things like input validation should be all in one spot and easy to follow and
read. if there's a lot of weird state you need to track you can manage that in
it's on class.

~~~
erikb
Yes, example time. I just wanted to make sure to get some general feedback
first.

I 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.

