Generally, if a software package is hard to install, configure, or to otherwise use, I avoid it. If it’s hard to maintain or even tell what’s happening when its running I avoid it. I’m not being arrogant, I’ve just done it wrong more than once and had to live with it and/or fix it. I’m speaking from experience; I’ve used a lot of overly complex systems. I’ll even confess that I’ve built some overly complex (awful) systems myself and it didn’t end well. That’s why I take such a strong view against complexity. In short, I actively avoid baroque needlessly complex systems and strive for simplicity. I see complexity as a sign of weakness in design and implementation.
Simplicity Equals Elegance
Simple software design often ensures success (both short term and long term), but it also helps to maintain the sanity of those who use that software. How much time have you spent wrestling to get a web server, application server, or database server up and running, and configured properly, for example? Or trying to solve an issue in a production system because you can’t tell what is happening and where it’s going wrong? It’s no accident that the main goal of Java EE 6 was to simplify Java EE development and deployment. Simplicity is also why thousands of developers have moved to other languages and environments such as Ruby to build web applications.
But when you need to connect more than one computer together, and arrange to have software communicate and coordinate activities between them, a certain level of complexity is unavoidable. For instance, a network stack consists of many low-level software components coordinating bits of data transmitted by fluctuations of electricity over a wire and though a network interface card. There’s very little simple about that, even at a high level. But computer science offers solutions to help here as well.
Abstraction or “Just wrap it!”
As a user or developer of network software, life is relatively simple today because operating systems and software libraries provide a nice facade to interact with. Behind these facades is typically a level of complexity most of us are not interested in dealing with, but thanks to clever design you may never have to. This is called abstraction, where you gain the ability to treat an entire system of seething complexity as a simple box in a diagram, or entity in your mind. Not everyone has the ability to abstract to the level required to deal with such things. Just as the most famous artists have used it to amaze us with their work, the best computer scientists use it to solve complex problems with (sometimes only apparent) simplicity. This is what computer scientists often refer to as elegance.
With this in mind I would suggest that the reciprocal idea is true too.
Elegance Equals Simplicity
Simple and elegant software design should go beyond just the code that makes the system work, it should extend to the user interface (or API) as well. This may be obvious for applications with a graphical interface, for which there are ample guidelines to good design, but it also applies to applications with more abstract interfaces: the tools and systems that we use to develop and deploy other software. For instance, why has FTP lasted so long in a world where constant software upgrades are a fact of life? Because it just works, out of the box. This is how all software should be. Don’t make installation, configuration, and usage an afterthought.
Turn your design inside out
A good internal design should be turned inside out, and it’s usage should be as elegant as it’s inner workings. If it’s pretty on the inside, it should at least be as pretty on outside. The inverse is true for those whose focus may be on the outside (i.e. graphic artists). If you’re a UI designer and also a programmer, try not to sacrifice the elegance of your software’s internal design for the sake of quickly iterating the UI. I’ve seen far too many prototypes (with hacked together code that was never meant to survive) become the actual product because of time constraints. Again, I speak from experience; this is another software crime I’m guilty of.
How do I tell if it’s “Elegant”?
Here are some questions to ask/answer yourself:
- Does it work? I’d be hard-pushed to call software “elegant” if it didn’t work.
- Is it easy to understand? Lots of the following factors can really be summarized by this one: if I can’t understand the code, it’s not elegant.
- Is it efficient? A bubble sort is just not elegant, because there are much more efficient algorithms. If a cunning algorithmic trick can drastically reduce the runtime, using that trick contributes to making the code elegant, especially if it is still easy to understand.
- Does it have short functions? – Long functions (more that 25 lines of code) make the code hard to follow. If I can’t see the whole function on one screen in my editor, it’s too long. Ideally, a function should be really short, less than 5 lines.
- Does it have consistent descriptive naming? – Short functions are all very well, but if functions are called foo, abc, or wrt_lng_dt, it can still be hard to understand the code. Of course, this applies to packages, classes, functions, and even APIs.
- Is there a clear division of responsibility? Is it clear which function or class is responsible for any given task or aspect of the design. Not only that, but a class or function should not have too many responsibilities — a class or function should have just one responsibility.
- Is there high cohesion? Cohesion is a measure of how closely related the data items and functions in a class or module are to each other. This is tightly tied in to division of responsibility — if a function is responsible for calculating primes and managing network connections, then it has low cohesion, and a poor division of responsibility.
- Is there low coupling? Classes and modules should not have have unnecessary dependencies between them. If a change to the internals of one class or function requires a change to apparently unrelated code elsewhere, there is needless coupling. This is also related to the division of responsibility, and excessive coupling can be a sign of too many classes, modules or functions sharing a single responsibility.
- Is there appropriate use of Object Oriented Design and other techniques? It is not always appropriate to encapsulate something in a class — sometimes a simple function will suffice, and sometimes other techniques are more appropriate. This is also related to the division of responsibilities, but it goes beyond that — is this code structure the most appropriate for handling this particular responsibility? Language idioms come into play here: is it more appropriate to use STL-style std::sort on an iterator interface, or does it make more sense to provide a sort member function? Can the algorithm be expressed in a functional way, or is an imperative style more appropriate? Are we “coding in XML” or “putting configuration in code”?
- Is there minimal code? Code should be short and to-the-point. Overly-long code can be the consequence of doing things at too low a level, and doing byte-shuffling rather than using a high-level sort algorithm. It can also be the consequence of too many levels of indirection — if a function does nothing except call one other function, it’s getting in the way. Sometimes this can be at odds with good naming — a well-named function with a clear responsibility just happens to be able to delegate to a generic function, for example — but there’s obviously a trade-off. Minimal code is also related to duplication — if two blocks of code do the same thing, one of them should be eliminated.
This is not an exhaustive list, but will help understand the concept of simple and elegant is software design and development.