Search This Blog

There Must Be A Better Way

As developers, we often try to defend our over-engineered and complicated solutions, and indeed, every problem has some amount of irreducible complexity. But there is often a simpler solution if we take the time to look for it. Part of good software development is believing in and spending the time to look for that simpler way. Don't resort to overly complex implementations just because the problem seems to call for it. The simple solution may not seem so shiny, but there is elegance in simplicity. It may not feel like you're practicing good engineering when you come up with a solution that looks too easy, but often times the simple solution is more robust and resistant to buggy edge cases.

In engineering and the sciences, simple is normally equated with elegant. A simple solution is easier to understand, maintain, and extend than a complicated one. The difficulty comes in finding a simple solution. It's hard work to understand a problem so thoroughly that the solution is made to look easy and obvious, but that is the mark of a good solution. All of that hard work may not be apparent in the end result, but it provides much more value than a complicated mess of code that people look at in awe because they can't believe that it even works.

Sometimes it seems like the problem is too complicated to warrant a simple solution. Other times the immense architecture of the complicated solution is a siren call that lures you in and blinds you to simpler alternatives. Finally, there are times when you stare in disbelief at the simplicity of the solution you came up with and convince yourself that such elegance isn't possible. Instead, you discard the easy option and pursue a more complicated path. I experience at least one of these traps with every engineering problem I work on, and it can be difficult to avoid them. Here are some things that I've found helpful in the quest for a better way.

Resist Over-engineering


Some programmers seem to revel in the intricate architectures that they create, claiming that the problem is so complex that a simpler solution is not possible. But what they've really created is an unmaintainable mess—a monstrosity that will collapse under its own weight before it ever gets close to shipping. You look at this pile of code and think, "there must be a better way," but the original architect can't see it because he's locked into the path he took and believes that the extra complexity adds necessary flexibility or extensibility.

Finding the simpler solution requires a completely different perspective and a willingness to solve only the problem at hand. A solution should be only as complex as the problem warrants. If the problem actually needs the added flexibility of the more complex solution, then it may be appropriate, but flexibility shouldn't be added for its own sake. Joel Spolsky railed against the unnecessary complexity touted by architecture astronauts when he praised pragmatic programmers that reject complicated frameworks and instead get stuff done. His advice is more pertinent than ever.

The basic idea is that design patterns, threading, templates, and other advanced programming techniques are complicated tools that should only be used when it is absolutely necessary. Don't use them just because you can. Use them when not using them will end up being more complicated. Deciding when to use the Factory pattern is a prime example of this concept. In theory, you could use the Factory pattern everywhere you instantiate an object, but that would clearly be overkill. On the other hand, situations arise where this pattern is genuinely useful. If you are instantiating similar objects from a class hierarchy in many places in your code, the Factory pattern will likely simplify your code instead of complicating it. That's when you want to use it.

I've been playing around on Exercism.io lately, and I've seen plenty of interesting examples of over-engineering in the solutions that programmers have submitted there. I thought about showing a couple examples here, but I don't want to single anyone out. Suffice it to say, some programmers submit verbose solutions with multi-level class hierarchies or delegation for simple problems like RNA transcription or converting decimals to Roman numerals. (Examples like these are plentiful on any coding site. Codewars.com and Project Euler are two more programming problem sites that allow you to compare your solutions with other programmers, and over-engineering is easy to find.)

If these programmers write such complicated code for simple problems, what do they do when the complexity of the problem scales up? I can only imagine. Complicated architectures have mental costs, and we only have so much brainpower to work with when programming. Why waste any of it on the overhead of an over-engineered, over-architected solution that solves more than the problem requires? It doesn't add clarity, and every time you have to deal with that code, it drains your mental reserves.

Find the Simple in the Complicated


Even after resisting the urge to over-engineer, the simple solution may still be elusive. Once the code is working, it will still be possible to make it better. I have not written or read a first-cut of a piece of code yet that couldn't be improved. That doesn't mean that every piece of code needs to be optimized, but if a part of the program needs to be extended or has issues that need to be resolved, finding a simpler solution is almost certainly an option.

I'm constantly amazed by the simplifications I miss when programming. While I didn't feel comfortable showcasing other programmers' code, I can certainly use my own, so here's my solution to the RNA Transcription problem on Exercism.
class Complement
  TO_RNA = {C:'G',G:'C',T:'A',A:'U'}
  TO_DNA = {C:'G',G:'C',U:'A',A:'T'}

  def self.of_dna string
    transcribe string, TO_RNA
  end

  def self.of_rna string
    transcribe string, TO_DNA
  end

  def self.transcribe string, transcription
    string.each_char.map { |c| transcription[c.to_sym] }.join
  end
end
The problem is fairly simple. It asks you to create a couple methods that return the RNA sequence for a DNA sequence (Compliment.of_dna) and vic versa (Compliment.of_rna). I thought this solution was pretty straightforward. I have named constant hashes for converting to RNA and to DNA, I use a generic transcribe method for both of the conversion methods, and the two conversion methods even read nicely, almost like English.

I was happy with the solution, but upon reviewing other submissions, I found that Ruby actually has exactly the method that I created as self.transcribe already included as part of the String class. I could simplify my solution quite a bit by using the String#tr method:
class Complement
  RNA = 'CGAU'
  DNA = 'GCTA'

  def self.of_dna string
    string.tr DNA, RNA
  end

  def self.of_rna string
    string.tr RNA, DNA
  end
end
Using the built-in method shaved four lines off the code and simplified the RNA and DNA constants. In the process of reviewing other people's code and striving to simplify my own, I learned about a Ruby String method that I hadn't known before and had some good practice expressing the essence of a particular problem. It's a great skill to cultivate.

Nearly all of the other problems I've solved have followed this same pattern. I try to come up with as simple of a solution as I can that directly expresses the core of the problem, and then I learn an even better way of doing it by reading other people's code. The same principle holds true for any programming that I do. If I need to find a better way of doing something, I can be sure that it exists; I just have to find it. The combination of Google and reading good programming books helps in the search for the problems I deal with outside of coding practice sites.

Don't Worry That the Simple Isn't Complicated


It may not feel like doing real work when you come up with a simple solution. You may feel guilty for producing something so obvious after a large investment of time! That's normal. Simple solutions are not something to be avoided because our egos believe that the problem warrants something more complicated. Don't confuse simple with simplistic. Simple solutions are more elegant, and are often more robust to edge cases and changing requirements than the overly-complicated alternatives. Simplistic solutions don't address all of the requirements and fall short of the problem's inherent complexity. We want simple solutions to complex problems.

If the simple solution works, don't worry that it looks small compared to the architectural edifice of a multiple-inheritance hierarchy. Coming back to Joel, he doesn't mince words on this issue:
You see, everybody else is too afraid of looking stupid because they just can’t keep enough facts in their head at once to make multiple inheritance, or templates, or COM, or multithreading, or any of that stuff work. So they sheepishly go along with whatever faddish programming craziness has come down from the architecture astronauts who speak at conferences and write books and articles and are so much smarter than us that they don’t realize that the stuff that they’re promoting is too hard for us.
Don't go along with the architecture astronauts if a simple solution will suffice. They may appear smarter, but they're also so deep in the complexity that they've created that they can't see that it will risk wasting everyone's time. A simple solution that works now and ships now is infinitely better than a complicated solution that will handle every possible future use case or additional feature but never ships.

Besides, complicated solutions always make me suspicious. A simple solution is a sign that the the problem is well-understood and the developers have thought hard about taking the right approach without generating any waste. Great programmers are like great teachers who really understand their subject and can teach it with a clarity of thought that makes complex material look easy. Great programmers will not complicate a design for the sake of over-engineering. They will search for the right solution that is appropriately simple and directly solves the problem. They don't worry about impressing others with complicated monstrosities when working software will prove their worth.

To be a great programmer, start believing that there must be a better way. Then find it.

No comments:

Post a Comment