112

I've spent a lot of time reading different books about "good design", "design patterns", etc. I'm a big fan of the SOLID approach and every time I need to write a simple piece of code, I think about the future. So, if implementing a new feature or a bug fix requires just adding three lines of code like this:

if(xxx) {
    doSomething();
}

It doesn't mean I'll do it this way. If I feel like this piece of code is likely to become larger in the nearest future, I'll think of adding abstractions, moving this functionality somewhere else and so on. The goal I'm pursuing is keeping average complexity the same as it was before my changes.

I believe, that from the code standpoint, it's quite a good idea - my code is never long enough, and it's quite easy to understand the meanings for different entities, like classes, methods, and relations between classes and objects.

The problem is, it takes too much time, and I often feel like it would be better if I just implemented that feature "as is". It's just about "three lines of code" vs. "new interface + two classes to implement that interface".

From a product standpoint (when we're talking about the result), the things I do are quite senseless. I know that if we're going to work on the next version, having good code is really great. But on the other side, the time you've spent to make your code "good" may have been spent for implementing a couple of useful features.

I often feel very unsatisfied with my results - good code that only can do A is worse than bad code that can do A, B, C, and D.

Is this approach likely to result in a positive net gain for a software project, or is it a waste of time?

13
  • 144
    i'll see your SOLID and rasie you YAGNI and KISS
    – jk.
    Sep 6, 2011 at 10:12
  • 16
    Boom, headshot.
    – Robert S.
    Sep 6, 2011 at 14:10
  • 16
    The problem of "over-engineering" is sometimes a manifestation of a requirements capture process that isn't working right. If you're struggling with "what-if's" and then answering them yourself WITHOUT the interaction of a stake-holder/client, that will put you in the position of predicting the future. Perhaps take some of that effort and put it back into understanding the requirements better before introducing more complexity into the code?
    – Angelo
    Sep 6, 2011 at 15:33
  • 4
    Maybe it's just me but I would consider "stupid" making my client/employer spend money to have something that they did not request/want :S Sep 6, 2011 at 15:39
  • 2
    It is almost never going to be harder to add a feature in the future when it is actually needed. You don't gain anything by doing it now. Sep 6, 2011 at 16:30

17 Answers 17

153

good code that only can do A is worse than bad code that can do A, B, C, D.

This smells to me like speculative generality. Without knowing (or at least being reasonably sure) that your clients are gonna need features B, C and D, you are just unnecessarily overcomplicating your design. More complex code is harder to understand and maintain in the long run. The extra complexity is justified only by useful extra features. But we are typically very bad at predicting the future. Most of the features we think may be needed in the future will never ever be requested in real life.

So:

Good code that can only do A (but it is doing that one thing simply and cleanly) is BETTER than bad code that can do A, B, C, D (some of which might be needed sometime in the future).

5
  • 89
    +1 for "we are typically very bad at predicting the future" The user will most likely ask for E :) Sep 6, 2011 at 11:57
  • 1
    +1 Couldn't agree more. I recently finished a work term at a company, and I would consider this the most important lesson I learned.
    – Wipqozn
    Sep 6, 2011 at 13:22
  • 4
    @Michał, is that a prediction? ;-) Sep 6, 2011 at 13:27
  • 10
    Even if they ask you for D, they will probably have a different mental model of it, and will ask for a slightly different kind of D which your abstractions don't support... Sep 6, 2011 at 16:40
  • "If a problem is not completely understood, it is probably best to provide no solution at all" or "Do not add new functionality unless an implementor cannot complete a real application without it." applies here. Via en.wikipedia.org/wiki/X_Window_System#Principles
    – nwahmaet
    Apr 23, 2012 at 20:20
87

Anecdote time:

I've had two developers work for me who leaned towards over-engineering in this manner.

For one of them, this basically ground his productivity to a halt, especially when starting up a new project. Most especially if the project was, by its nature, fairly simple. Ultimately a piece of software that works now is what we need. This got so bad that I had to let him go.

The other developer who was prone to over-engineering made up for it by being extremely productive. As such, despite all the extraneous code, he still delivered faster than most. However, now that he's moved on, I often find myself irritated at the amount of extra work that is needed in order to add functionality as you have to modify (entirely unnecessary) abstraction layers etc.

So the moral is, over-engineering will eat up extra time that could be better spent on doing useful things. And not just your own time, but also of those who have to work with your code.

So don't.

You want your code to be as simple as possible (and no simpler). Handling 'maybes' isn't making things any simpler, if you guess wrong you will have made the code more complex with no real gain to show for it.

5
  • 10
    +1 for as simple as possible but no simpler.
    – Julian
    Sep 6, 2011 at 11:17
  • 34
    "A designer knows he has achieved perfection not when there is nothing left to add, but when there is nothing left to take away." Antoine de Saint-Exupery
    – Arkh
    Sep 6, 2011 at 12:16
  • avoid duplication like the plague and you won't go far wrong.
    – Kevin
    Sep 6, 2011 at 15:33
  • @arkh...I was going to use the same quote :P Sep 6, 2011 at 20:44
  • 6
    I'd put it this way: Every line of code has a high cost associated with it. So, minimize costs by minimizing code. Deleting unnecessary code is as productive (maybe even more productive) than writing new code. Sep 7, 2011 at 1:10
26

SOLID and KISS/YAGNI principles are near-polar opposites. One will tell you that if doSomething() can't be justified as an integral part of the job the class that calls it is doing, it should be in a different class that is loosely coupled and injected. The other will tell you that if this is the one place doSomething is used, even extracting it into a method may have been overkill.

This is what makes good programmers worth their weight in gold. "Proper" structure is a case-by-case basis, requiring knowledge of the current codebase, the future path of the program, and the needs of the business behind the program.

I like to follow this simple three-step methodology.

  • On the first pass, make it work.
  • On the second pass, make it clean.
  • On the third pass, make it SOLID.

Basically, this is how you blend KISS with SOLID. When you first write a line of code, for all you know it'll be a one-off; it simply has to work and nobody cares how, so don't get fancy. The second time you put your cursor in that line of code, you have disproved your original hypothesis; in revisiting this code, you are likely extending it or hooking into it from somewhere else. Now, you should clean it up a little; extract methods for commonly-used tidbits, reduce or eliminate copy/paste coding, add a few comments, etc etc. The third time you come back to this code, it's now a major intersection for your program's execution paths. Now you should treat it as a core part of your program, and apply the SOLID rules where feasible.

Example: You need to write a simple line of text to the console. The first time this happens, Console.WriteLine() is just fine. Then you come back to this code after new requirements also dictate writing the same line to an output log. In this simple example, there may not be a lot of repetitive "copy/paste" code (or maybe there is), but you can still do some basic cleanup, maybe extract a method or two to avoid inlining IO operations into deeper business logic. Then, you come back when the client wants the same text output to a named pipe for a monitoring server. Now, this outputting routine is kind of a big deal; you're broadcasting the same text three ways. This is the textbook example of an algorithm that would benefit from a Composite pattern; develop a simple IWriter interface with a Write() method, implement that interface to create ConsoleWriter, FileWriter and NamedPipeWriter classes, and one more time to create a "MultiWriter" composite class, then expose an IWriter dependency on your class, set up the MultiWriter composite with the three actual writers, and plug it in. Now, it's pretty SOLID; from this point forth, whenever the requirements dictate that output should go anywhere new, you just create a new IWriter and plug it in to the MultiWriter, no need to touch any existing working code.

6
  • Agreed, but usually once you get past the first step you never get to go back to the second or third, because now the feature is "live" and there are more features coming down the pipe so you can't go back and fix the first feature. You find that all you can do is the first step with every feature and then you're left with a swamp. Sep 6, 2011 at 17:25
  • 2
    @Wayne M - It can certainly happen that way in waterfall SDLC or in short-time scenarios. In those cases, getting the job done according to the original specs by the deadline is what is being valued, not the maintainability of code. If you want to value the quality of source code, you simply must build time into the schedule for refactoring. Much like if you value the quality of content in any other job involving production of written information, you build time in for proofing and editing.
    – KeithS
    Sep 6, 2011 at 19:19
  • Your opening line is misleading -- you say they are opposed, then describe the best way to used them together. I don't know if I should downvote for the bad line or upvote for the good advice. Sep 13, 2011 at 12:30
  • 1
    I don't think I contradicted myself. They are near-polar opposites; religious adherence to one or the other will necessarily mean rejection of the other. That does not mean that they are mutually exclusive, however; you don't have to choose to adhere 100% to either methodology. That was the point of the rest of the answer; how do you strike a balance when doing so involves give-and-take from each principle's conclusions?
    – KeithS
    Sep 14, 2011 at 19:46
  • Really nice process: works, clean, SOLID. I wonder if a corollary of this is "don't try and engineer anything without a hacked up prototype first". May 2, 2012 at 4:30
17

good code that only can do A is worse than bad code that can do A, B, C, D.

1) Have a code which only do what is supposed to do.

If I feel like this piece of code is likely to become larger in the nearest future, I'll think of adding abstractions, moving this functionality somewhere else and so on.

2) If you plan your code to do A, B, C and D, the customer will ultimately ask you for E.

Your code should do what is supposed to do, you shouldn't think now about future implementations, because you'll never end changing your code continuously, and more important you'll over-design your code. You should refactor your code as soon as you feel it needs it because of present features, not struggling to get it prepared for something that it's not yet gonna do, unless of course you planned it as part of the project architecture.

I suggest you to read a good book: The Pragmatic Programmer. It will open your mind and teach you what you should do and what you shouldn't do.

Also Code Complete is a great resource full of useful informations that every developer (not only programmer) should read.

12

very time I need to write a simple piece of code, I think about future

Maybe here is the problem.

At early stages, you have no idea of what would be the final product. Or if you have it, it's wrong. For sure. It's like a 14 years old boy who asked a few days ago on Programmers.SE if he must, for his future career, choose between web apps and I don't remember what else: it's pretty obvious that in a few years, the things he like will change, he will be interested by other domains, etc.

If, in order to write three lines of code, you create a new interface and two classes, you're over-engineering. You'll obtain a code which will be difficult to maintain and difficult to read, just because for every useful line of code, you have two lines of code you don't need. Not counting XML documentation, unit tests, etc.

Think about it: if I want to know how a feature is implemented in your code, would it be easier for me to read through twenty lines of code, or it would be faster and easier to have to open dozens of semi-empty classes and interfaces to figure out which one uses which, how are they related, etc.?

Remember: larger codebase means more maintenance. Do not write more code when you can avoid it.

Your approach is also harmful on other sides:

  • If you need to remove a feature, isn't it easier to figure out where a specific twenty lines method is used, than to waste your time to understand the dependencies between dozens of classes?

  • When debugging, isn't it easier to have a small stack trace, or do you prefer to read dozens of lines in order to figure out what is being called by what?

To conclude, it seems similar to premature optimization. You're trying to solve the problem without even being sure if there is a problem in a first place, and where it is. When working on version 1 of your product, implement features you need to implement right now; don't think about something you expect to implement in two years in version 14.

1
  • "dozens of semi-empty classes" have the additional disadvantage that there is not enough material to understand what these classes are good for.
    – gnasher729
    Jan 15, 2017 at 19:40
5

Writing lots of code that will (probably) never be used is a very good way to get issued with a P45. You do not have a crystal ball and have no idea as to the final direction that the development will take so spending time on these functions just costs money for no return.

3

Trying to predict what may be needed from code in the future often ends up being needless over-engineering (a habit I'm currently trying to shake). I would say just do the three lines. When the need arises (and not before), refactor. That way your code always does what it needs without being over complicated and evolves good structure naturally through refactoring.

3

I often say that coding is like the light side/dark side of the Force - the "light" side requires more effort but yields greater results. The "dark" side is quick and easy and gives greater benefits immediately but corrupts you down the road. Once you start down the dark path, forever will it dominate your destiny.

I run into this all the time, at every job I've ever had, it's like a curse I cannot escape. The company culture is always the path of the dark side, and quick hacks/fixes to push out new features, and my pleas and cries of refactoring and writing code properly falls on deaf ears, if it doesn't lead to my termination for "trying to change things" (no joke I have had that happen a handful of times, because I wanted to introduce design patterns and move away from quick hacks).

The sad truth is that often the stupid/dark side way is the way you have to tread, you just have to make sure to tread lightly. I've slowly and sadly realized that programmers who understand the right way to write software, i.e. following SOLID, using design patterns, obeying SoC, etc. are much less common than the clueless hacks who will write an if statement to fix a bug, and when more bugs arise just add onto that statement instead of ever thinking "Maybe there's a better way of doing this" and refactoring the code to be properly extensible.

3
  • 3
    if is a lot more easier to maintain than IAbstractBugFixer from an IAbstractBugFixerFactory. When, and, IF, you get to add a second if, then it's time to refactor. Design patterns and SOLID are very important during architecture phase, but not when the product is already running and is written in one style that all team members have agreed on.
    – Coder
    Sep 6, 2011 at 13:24
  • @Coder Try not to assume architecture cannot change at any time. It can and does. Mar 26, 2012 at 0:31
  • 1
    Wayne M, I can empathize with your work situations. Stay with the Force. :)
    – Jennifer S
    Jun 20, 2012 at 18:08
3

Being aware of what might happen (future) is NEVER bad. Thinking about what might be a better way to do something is part of what makes you good at your job. The harder part is determining whether the time spent:payoff ratio is justified. We've all seen situations where people do the "easy if" to stop the immediate bleeding (and/or yelling), and that as those add up, you get confusing code. Many of us also have experienced overdone abstraction that is a mystery once the original coder moves on, which also produces confusing code.

I would look at your situation and ask these questions:

  1. Is this code stuff that is mission critical, and will it be significantly more stable if I re-code? In surgery speak, is this refactoring lifesaving, or is it merely elective and cosmetic?

  2. Am I considering refactoring code that we're going to replace in 6 months?

  3. Am I willing to take as much time to document the design and my reasoning for future developers as I am to spend doing the refactoring?

  4. Regarding my elegant design for adding future features, is this in code that users request changes to every week, or is this the first time I've touched it this year?

There are times where YAGNI and KISS win the day, but there are days where a fundamental change will get you off the downward spiral to crappiness. As long as you are realistic in your assessment of not only what you want, but what others will have to do to maintain your work, you'll be better able to determine which situation is which. Oh, and don't forget to write down what you did, and why. It'll save those who follow you, but also yourself when you have to come back later.

3

In the second edition of Stroustrups 'The C++ programming language', (I don't have the page available) I read

Don't add code spontaneously in place.

and I went good following the advice. Of course there are tradeoffs, and you have to find a balance, but short fragments are better testable than a big spaghetti mess.

I often experienced, that while differentiating from one case to two cases, if you think about 2 as n-cases, you open up a door to many new possibilities, which you might not have thought about.

But then you have to ask the YAGNI question: Is it worth it? Will it really be useful? Being experienced means, that you will be rarely wrong, and as a beginner more often wrong.

You should be critical enough to recognize a pattern, and detect, whether your code is hard to maintain, because of too much abstraction, or hard to maintain, because everything is solved in place.

The solution is not this or that, but to think about it. :)

2

"good code that only can do A is worse than bad code that can do A, B, C, and D."

This may make some sense in product development; But most of the IT professionals work in 'projects' rather than product developemnt.

In 'IT projects', if you program a good componenet, it will function smoothly for the life time of the project - which may not run longer than 5 or 10 years by then the business scenario might have beome obsolete and a new project/ERP product might have replaced it. During this 5/10 year life span, unless htere are defects in your code, no one will notice it's existance and merits of your best thoughts are gone unnoticed! (unless you are good in beting your own trumpet loud!)

Not many get an opportunity to program the 'Windows Ctl+Alt+Del' and those few get that chance may not realise the future-potential of their code!

1

Many books on lean and/or agile development will help reinforce this approach: do what is necessary right now. If you know you're building a framework, then add in abstractions. Otherwise, don't add complexity until you need it. I recommend Lean Software Development, which will introduce many other concepts that can make a substantive difference in productivity.

1

It's funny how people talk about right way / wrong way of doing things. At the same time the task of programming is still painfully difficult, and gives no good solutions for writing big complex systems.

It may be that someday we, programmers, will finally figure out how to write complex software. Until then I suggest that you always start with "stupid" prototype implementation first, and then spend just enough time on refactoring so that your colleges can follow your code.

2
  • 1
    In most cases, you'll never have some special time for refactoring - that's probably the main reason for all these "We can't implement this without re-implementing that" ;-) You either do it the right way from the very beginning, or you do it the wrong way all the time. Sep 6, 2011 at 14:07
  • @loki2302: Remember that writing new code is always easier. You will be twice as fast when prototyping stupid code, after which your productivity will drop to zero for about half the time. So in the end you will still be just as fast the programmers trying to design the right way..
    – AareP
    Sep 6, 2011 at 14:25
1

Having seen such prematurely generalized designs that did not fit at all to the actual requirements that came later, I devised a rule for me:

For hypothetical requirements only write hypothetical code.

That is: you are well advised to think about changes that might occur later. But only use these insights to choose a design for the code that can easily be changed and refactored if those requirements actually come up. You may even write some code in your head (hypothetical code) that you would want to write in that case, but do not write any actual code!

0

I think the mindset that will help you is to always strive for concrete solutions to coding problems instead of abstract solutions. Abstractions should only be added when they actually help simplify the code base (when for instance they allow you to make the code base smaller).

Many programmers have found that abstractions emerge almost on their own, when they DRY their code up. Design Patterns and Best Practices help you to find opportunities for doing just that, but are not in themselves goals worth pursuing.

0

I think that over-engineering often seems from insecurity about writing code. All abstract principles and patterns should be treated as tools to help you. What often happens is that they are treated as standards, one must conform to.

I believe that a programmer is always in a better position to decide how to abstract than an axiom.

The rest has already been said by KeithS

2
  • I think one way to get self-security about writing code is coding as root on a linux system. If you type willy-nilly, boom, you will just have to reimage the VM. Teaches all kinds of good habits real quick. Make sure your box lives in the real internet, and make sure you look at the logs. (ssh, http) Those are real fun too! Sep 7, 2011 at 22:52
  • My version is: Ignore principles you don't understand. If you decide to use them, don't treat them any more seriously than you would an exercise.
    – sabof
    Sep 7, 2011 at 23:16
0

Ask yourself what the advantages of a good design are:

  • Easier to understand
  • Easier maintains
  • Portable
  • Stays useful for a long time
  • Easy to add new features

Now, ask yourself if adding all those layers of abstractions really add to any of the points mentioned above. If it does not, you are doing it WRONG.

If you are able to add a new features by adding 3 lines of code like this:

if (condition()) {
  doSomething()
}

Then please, please do so. This indicate your previous design was good and easy to adapt. Only once your classes start to grow beyond a certain extend, use refactoring to split functions and possibly extract new classes.

My rule of thumb is that new features should be implemented as minimalistic as possible, only when something is to big to grasp upfront (lets say, takes more then 1 day or half a day to implement) you can add a rough global design. From there on, only add abstraction layers when the size of the code grows. You then refactor afterwards! After a while it should even come natural to you when you need to design a piece a little more, or go for the quick road. Another indication some code can use some cleanup is when you reuse it. Each time you add a new feature, or call old code in a new place, it's a good time to look at the old code and see if you can improve it a little before you add to it again. This way, hot code will slowly become more cleanly, while the uninteresting parts slowly rot away and don't take any of your valuable time. (If you use some old code, and you fail to quickly understand it, this might be a demotivation to improve on it, but remember that is also a big warning some cleanup/redesign is appropriate).

If you work like this, you will never over-design anything. It might take some discipline to go back to old code when you want to add something new, or to leave new code slightly more ugly then you like, but you will be working towards something productive instead of over engineered.

Not the answer you're looking for? Browse other questions tagged or ask your own question.