Wednesday, May 6, 2009

Code Cannot Be Simpler than the Problem Domain

Most of us have decided that code simplicity is a desirable quality. However, we have also observed that several things limit the simplicity of our code and even exaggerate our code's complexity. These things that make our code more complex than it should be include overuse/abuse of design patterns, overuse/abuse of favorite syntax/code features (such as implementation inheritance), resume-driven development, the Magpie Effect, Not Invented Here Syndrome, and other dysfunctional motivations.

Although many of us see the benefits of striving for simplicity and avoiding unnecessary complexity, I have also observed that in some cases we may overexert ourselves in an effort to achieve simplicity in our code that is not justified by the problem domain being modeled in the code. A general rule that seems obvious but which we often seem to forget is that the code cannot be simpler than the problem domain it describes. It is typically a vain effort to try to write very simple code for a very complex problem domain. We can strive our best to make the code as simple as possible, but the size and complexity of a problem cannot be completely ignored. At some point, the complex functionality must be implemented. As an example, the code for implementing addition should not be as complex as code for implement Fast Fourier Transforms. We can strive to make the FFT implementation as simple as possible, but it will always have a certain degree of complexity simply by the virtue of the problem's own complexity.

We can do some things that make the code appear to be more simple than the problem domain it describes. I describe each of these here, explain how each of them doesn't really make the code simpler than the problem domain, and briefly discuss the tactics we regularly employ to enjoy these perceived simplicity benefits.

Simplifying Assumptions

In a particularly complex problem domain, one of the tactics we can use to make our code simpler is to leverage simplifying assumptions. Note that we've not really written code that is simpler than the problem itself by doing this. Instead, what we've really done is simplified the problem domain to a point where its code can be simpler. These simplifying assumptions not only make our design and code effort easier, but they often make for a more usable tool or software product for our end users.

Codifying the Problem

Code can sometimes appear to be simpler than the problem domain it models because of the fact that we are better at reading the code than we are at reading the other descriptions or design documents related to a particular problem. This does not necessarily mean that the code is simpler than the problem space; it only means that we are more comfortable reading code than we are reading our types of documents. Another person might be more comfortable with the written description. A central takeaway from this is the value of prototyping. Prototyping helps us to flesh out questions and risks in the problem space. The prototype also codifies our understanding of the problem space. Similarly, willingness to refactor is also important in this area because we learn as we codify the problem.

Leveraging the Work of Others

Today, many enterprise functions are provided by an underlying application server, library, toolkit, framework, or domain-specific language. It can seem to the developer using these that many of these enterprise functional benefits come very easily and simply. While they may be easy for the end developer, the overall code (including the code in the product being used) is far from simple. The use of an existing library, framework, toolkit, application server, or other pre-built piece of code does give the impression of greater simplicity thanks to previous work. What this tells us, of course, is the value of reusing good products and not reinventing the wheel unnecessarily.


The effort to achieve coding simplicity in the light of a complex problem domain reminds us of the value of prototyping, of refactoring, of using pre-built libraries and frameworks, and of using simplifying assumptions. It is also important to realize that some problems are so complex and so large that even the simplest, cleanest code may still be rather large and complex to adequately meet expectations. It is admirable to make the code as clean and simple as possible, but the unavoidable fact is that some of the things we must describe in code are themselves complex and their code implementation will have to be complex as well (unless we allow simplifying assumptions as described above).


Rick Minerich said...

Code can be as simple as the subset as the problem domain which you need to represent to solve the particular problem you are facing. This is almost never the data needed to represent the entire domain.

ferrisoxide said...

Hi Dustin

You make some good points here, but I worry when I read things like "code cannot be simpler than the problem domain it describes". The issue I have is that when we're coding we're not actually describing the problem space - or at least we shouldn't be. We should be describing the solution space.

The complexity of the solution shouldn't be the same order of magnitude as the problem. A penny-drop moment occurred for me years ago when I was first exposed to Test-Driven Design (TDD). In the (now classic) Bowling Game scenario, it's quite logical to deconstruct a bowling game into Game, Frame Throw, etc classes. These are the objects that represent the "problem". But following a straightforward analysis of the problem won't necessarily lead you to a simple solution. You can follow the original discussion here:

The interesting outcome of resolving this problem using TDD is a simpler model than the problem would initially suggest. Indeed the solution is expressed in an altogether different set of concepts than the original problem domain.

This is the sort of stuff I see time and again: we often try to express the problems we are trying to solve using the terms and concepts from the problem space - ending up codifying behaviours and data structures that reflect the complexity of the problem. The language of the solution can often be completely orthogonal to the language used to describe the problem, and in some cases orders of magnitude simpler.

Dustin said...

Rick and ferrisoxide,

Thanks for the feedback. You both make excellent points. In fact, I think both your points (focus only on the applicable subset of the problem and on the solution for that particular problem subset) would have made excellent fourth and fifth points in my blog. Thanks to your feedback comments, these points have now been made.