Why Simple is So Complex

The complexity of simplicity in software engineering

I conclude that there are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies and the other way is to make it so complicated that there are no obvious deficiencies.— Tony Hoare

Simplicity. The software engineer’s Holy Grail. We are repeatedly urged to seek it by those in high places:

(Software design is) a craft… and it has a lot to do with valuing simplicity over complexity — Barbara Liskov

We need to build simple systems, if we want to build good systems — Rich Hickey

The only way to write complex software that won’t fall on its face is to hold its global complexity down, to build it out of simple parts — Eric S. Raymond

Simple can be harder than complex: You have to work hard to get your thinking clean to make it simple. But it’s worth it in the end because once you get there, you can move mountains. — Steve Jobs

Simplicity is a prerequisite for reliability.— Edsger Dijkstra

The price of reliability is the pursuit of the utmost simplicity— Tony Hoare

And yet, like the grail, simplicity proves elusive. There are many reasons for this:

Software is essentially complex. The elements of software interact with each other in a nonlinear fashion. The complexity of the whole increases much more than linearly.

You don’t always have control over the software you must use (e.g., third party libs, libs from other groups in your company, legacy libraries, &c.)

There are constant pressures for change.

Competing needs and interests often conflict. (1)

The complexity of the whole increases much more than linearly.

Non-technical Factors

Much of the complexity that [a programmer] must master is arbitrary complexity, forced without rhyme or reason by the many human institutions and systems to which his interfaces must conform. — Fred Brooks

Whenever we think about a piece of software, we have to think beyond the technical. There are reasons it exists, and they have nothing to do with code.

Some of these things include:

Users

Business and stakeholders

The needs/interests of engineers, designers, product owners

Financial constraints

In many cases, the drivers behind these complications will come from personal interest or politics. While we may not like this, these are real factors that impact software development. We ignore their influence and complexity at our peril.

Complexity is Inevitable

In the long run every program becomes rococo — then rubble — Alan Perlis

In any meaningful software program, complexity is inevitable. Software systems must evolve, or they become obsolete. As they evolve, they inevitably add complexity in the form of new or changed features. So, as it turns out, it is in our interest to introduce complexity into our software!

Simplicity Cannot Come from Simple Thinking

In an industry riddled with complexity, it would be very comforting if there were a way to guarantee good software. Indeed, many will claim that a magical technique or method exists. Let’s examine a few of these:

Static Types

Frameworks

Static Types Will Solve Everything!

Much hype, and much effort of late has been put into Static Types as the savior of software quality. For example, to quote the Elm website:

Unlike hand-written JavaScript, Elm code does not produce runtime exceptions in practice. Instead, Elm uses type inference to detect problems during compilation and give friendly hints. This way problems never make it to your users.

That’s a lovely sentiment. If only it were true. It would be amazing if the correct use of static types could solve all our problems. Of course, if that were true, it would be a moral imperative of every software developer to use a language such as Elm. The impact would be so significant, so earth shatteringly vital there would be no debate. Unfortunately, that’s not the case.

Static typing fanatics try to make us believe that “well-typed programs cannot go wrong”. While this certainly sounds impressive, it is a rather vacuous statement. Static type checking is a compile-time abstraction of the runtime behavior of your program, and hence it is necessarily only partially sound and incomplete. This means that programs can still go wrong because of properties that are not tracked by the type-checker, and that there are programs that while they cannot go wrong cannot be type-checked. — Erik Meijer and Peter Drayton, from The End of the Cold War Between Programming Languages

In fact, as Meijer and Drayton note, an overemphasis on Static Types can cause problems, because of the increased complexity they introduce:

The impulse for making static typing less partial and more complete causes type systems to become overly complicated and exotic as witnessed by concepts such as “phantom types” and “wobbly types”. This is like trying to run a marathon with a ball and chain tied to your leg and triumphantly shouting that you nearly made it even though you bailed out after the first mile.

Data around bug density suggests that their conclusions are accurate. The correlation between languages and lower bug density more closely matches on simplicity than static types:

As Dan Lebrero, the author of the research cited above, notes: “The [data] show no evidence of static/dynamic typing making any difference, but they do show… a gap between languages that focus on simplicity versus ones that don’t.”

You Just Need to Use the Right Framework

A programming language is low level when its programs require attention to the irrelevant. — Alan Perlis

The software industry is constantly working to provide abstraction layers that make coding easier and more reliable. The mere fact that we no longer code in machine language is a testament to the success of this endeavor. While we have removed a lot of accidental complexity, the core complexity inherent in systems remains. And the abstractions we’ve built give rise to further problems.

As Joel Spolsky famously noted: “All non-trivial abstractions, to some degree, are leaky.” By “leaky”, he means that the abstraction somehow fails to manage the complexity of what it was designed to simplify. The complexity still “leaks” through the cracks in the abstraction.

We can see a modern-day example of this with React JS. One of the main intentions of React is to abstract away the DOM. However, React provides refs for those cases where you may need to interact with the DOM. To do so, of course, you must understand how to interact with the DOM. This means that the abstraction has failed. In fact, it introduces the additional complexity of having to understand how to “escape” React to interact with the DOM.

Then, too, there are complications arising out of the abstraction. Abstractions of necessity hide the details of implementation, and, therefore, place limits on what can be accomplished. This is the reason why React has refs . There’s things it just can’t do. The abstraction, itself, may have inherent complications.

There is no Silver Bullet

While the comments above may raise the hackles of religious bickering, they are not intended to. The intent, rather, is to point out:

There is no single development, in either technology or in management technique, that by itself promises even one order-of-magnitude improvement in [software] productivity, in reliability, in simplicity. — Fred Brooks

However, unlike entering Dante’s Inferno, you need not abandon all hope. Recognizing that there is no magic method for building perfect software gets you out of the realm of magical thinking. It is only when you come out of this fantasy that you can deal with the reality of software. And, once you’re there, you’ll see that it’s not about software.

It is larger: It’s personal, and interpersonal. It’s procedural. It’s environmental and situational. It’s temporal and historical. It’s multi-generational and immediate. In other words… It’s about systems.

If there is no Silver Bullet, what can we do? We can accept that:

There will always be complexity for us to manage.

We will not write perfect software — and be OK with that.

There will always be compromise and imperfection.

Good work takes time.

We must deal with human factors (preferences, politics, different ways of thinking. &c.).

…design is something that is best done slowly and carefully — Alan Kay

What Makes it Simple?

[Software simplicity] demands the same skill, devotion, insight, and even inspiration as the discovery of the simple physical laws which underlie the complex phenomena of nature. It also requires a willingness to accept objectives which are limited by physical, logical, and technological constraints, and to accept a compromise when conflicting objectives cannot be met. — Tony Hoare

What makes something simple is hard to pin down. Part of this is because simplicity is somewhat subjective. Part of this is because the interpretation of simplicity is dependent on experience. Something simple and obvious to a trained mathematician may be not be so simple for a biologist — and vice versa. This leaves us, at best, with a vague understanding of simplicity that is reminiscent of Justice Potter’s famous definition of pornography: “I know it when I see it.”

That said, here are some aspects of a simple program:

Comprehension and problem solving are manageable once the principles underlying the system are well understood.

The system makes use of well-known cultural markers to ease comprehension.

It maintains design consistency.

It makes good use of semantics.

It balances the needs of flexibility with the boundaries and domain of the system.

Complexity is circumscribed as much as possible.

Unnecessary complexity is identified, and removed.

Protecting Against Unnecessary Complexity

While there are things individual developers can do to prevent complexity, it is efforts at the organizational level that have the most impact. Here are a few things organizations can do:

Haste Makes Waste

When any… project is nearing completion, there is always a mad rush to get new features added… The rush is mad indeed, because it leads into a trap…— Tony Hoare

Strive to get buy in from Sales, Business, and Product that software development — good software development — takes time.

Allow time for design.

Allow time for good coding practices.

Allow time for refactoring.

Every sacrifice has a cost. These costs add up over time.

Avoid The Latest Tech Trap

Software development is a fashion industry. Chasing after the latest and greatest introduces chaos as programmers struggle to learn the methodology, language, or framework that will finally allow them to do things the “right” way. Thoughtfulness, and a willingness to not follow the bandwagon simply because it’s popular, can go a long way towards protecting the sanity of your code.

Proven is a lot better than cool.

Follow the Standard Procedures

Programmers are not to be measured by their ingenuity and their logic but by the completeness of their case analysis. — Alan Perlis

There are standard, well-known, battle-tested practices that help with software quality and simplicity:

Code Reviews — perhaps one of the most powerful tools in a software team’s tool box. Make sure your code reviews are well thought out, and well executed.

Design Reviews — don’t just review code, review designs as well.

Enough Design Upfront — don’t just sit down and code. Look at how your system is designed. Follow those design patterns to ensure your designs stay consistent over time.

Have a grand plan for how your system will function. Define aspects at a high-level such as: How will you handle asynchronicity in your application? How will you manage fault tolerance? How will you manage data consistency? &c.

Make sure your requirements are good — however you get them.

Summary

Simplicity is far from simple. It’s also important to remember that it is a means to an end, and not the end itself. The end being quality software that is easy to use, and easy to work with.

To date, keeping systems simple has been one of the most effective ways that humans have managed working with software. Simplicity is as much a process and practice, as it is a set of principles, guidelines, or a philosophy. It’s something that should be championed and managed by the entire organization.

— — — — —

Further reading: