36

How do you guys know that you are writing the most robust code possible without overengineering?

I find myself thinking too much about every possible path that my code can take, and it feels like a waste of time sometimes. I guess it depends on the kind of program you are writing, but I don't want to use too much of my time taking situations into account that will never happen.

6
  • 2
    Code it in Agda2
    – SK-logic
    Sep 23, 2011 at 13:52
  • a concrete example would greatly help making your point. :) Sep 23, 2011 at 17:37
  • Can I just check that you really are asking about robustness, i.e. "the ability of a system to continue to work in the presence of invalid inputs or stressful environmental conditions", because some answers seem to think you are talking about extendibility. Sep 23, 2011 at 20:36
  • I work under crazy deadlines plus it is for demo-ware, so I can happily hack away fast without perfection paralysis.
    – Job
    Sep 23, 2011 at 22:33
  • 1
    Here is an article that talks about the subject: code-tag.com/2017/04/02/… Apr 11, 2017 at 6:18

8 Answers 8

41

How do you guys know that you are writing the most robust code possible without overengineering?

What do you consider robust code? Code that is already future proof and so powerful that it can deal with any situation? Wrong, no one can predict the future! And wrong again, because it'll be a complicated, unmaintainable mess.

I follow various principles: First and foremost YAGNI (yet) and KISS, so I don't write unecessary code. That also effectively prevents overengineering. I refactor the application when extensions are needed. Modern refactoring tools let you quite easily create interfaces and exchange implementations afterwards when you need them.

Then I try to make the code I write as robust as possible, that includes eliminating as many paths the program can take (and also states) as possible and a bit of Spartan programming. A great help are "atomic" functions/methods that do not rely on external states or at least don't leave the program in an inconsistend state when they fail. If you do that well, it's also very unlikely that you'll ever end up with spaghetti code and it's a blessing for maintainability, too. Also, in object oriented design, the SOLID principles are a great guide to robust code.

I've really found out that often times you can reduce complexity, for example combinatorial explosions of program paths or states, by deeply thinking about how you could design it as the straightest path possible. Try to keep the possible combinations at a minimum by choosing the best ordering of your subroutines and designing them for this purpose.

Robust code is most always simple and clean code, but simplicity is a trait that is not always easily achieved. Yet, you should strive for it. Always just write the simplest code possible and only add complexity when you have no other choice.

Simplicity is robust, complexity is fragile.

Complexity kills.

19
  • 2
    Because it doesn't have tons of classes, factories and abstractions. It's a paradox, but some people like that stuff. No idea why.
    – Coder
    Sep 23, 2011 at 13:08
  • 5
    This is Sparta!!! Sep 23, 2011 at 16:21
  • 4
    People that haven't been doing this for twenty years just do not get how complexity can kill you. They think they are so smart. They are dumb, not smart. That complexity is going to kill you dead. Sep 23, 2011 at 17:06
  • 7
    Unless the questioner is using 'robust' in a different sense from the one I understand, you are answering a different question. He is not asking "should I code to allow for future requirements" he is asking "what unusual input cases should I handle". None of YAGNI, KISS and SOLID are relevant. Do you need to allow for a million users trying to log on simultaneously? What will happen if a login name starts with a backslash? None of these questions are answered by YAGNI. Sep 23, 2011 at 20:43
  • 2
    SOLID is against YAGNI and KISS. If people prioritize SOLID over YAGNI and KISS, they create code so obscure and full of artificial abstractions that it's impossible to understand a thing without reading dozens of source files. The only thing one needs to stick in SOLID is the "L" - Liskov substitution. All others may imply in trade-offs that can be worst. Sometimes a single "if" is desired against a plugable design full of classes and interfaces that have nothing to do with the business domain.
    – fernacolo
    Dec 10, 2016 at 5:00
8

I try to keep a balance, focusing on

  • handling all possible execution paths in the existing use cases (this is the "robustness" part),
  • enabling features / requirements I am fairly sure that are going to come in the foreseeable future, and
  • things I know from experience that are going to be needed for long term maintainability of the code base (i.e. keeping the code clean and testable).

It is a fuzzy border area - sometimes I manage to do some unneeded work, sometimes I fail to do something which turns out to be necessary later. If the misses aren't big, I am fine. At any rate, I strive to learn from my mistakes.

1
  • An example would be great here handling all possible execution paths in the existing use cases
    – CodeYogi
    Oct 19, 2015 at 14:14
5

The difference between robust and overengineering is the difference between gracefully handling all possible use cases, even the bizarre and fringe use cases that SHOULDN'T happen. When I say gracefully I mean, the user enters a bizarre exception case or runs into a situation which calls for an unsupported or unspecified feature that wasn't defined and the code gracefully exits without crashing or informs the user of unsupported functionality.

Overengineering on the other hand could fall in the realm of complete implementation of features that were not needed or asked for (some features the client does NEED but were never asked for!) OR it can be defined by deriving an overly complex design or overly complex code to handle a relatively simple problem.

4

1) Get requirements.

2) Write minimum code to meet requirements. If something is ambiguous, make an educated guess. If it is super ambiguous, go back to 1.

3) Send to testing.

4) If the testers say its good, document the approval. If something is off, go back to 1.

Focus on passing tests, not predicting tests. If you do not have testers...get testers! They are essential not only to verifying code correctness, but to the entire development process.

2
  • 1
    +1 for Focus on passing tests, not predicting tests, however many developers like myself are expected to do both however in the lack of strong business analysts.
    – maple_shaft
    Sep 23, 2011 at 12:44
  • @maple_shaft - Very true. The point is that these problems arise because of someone else's incompetence. Stressing out over someone else's job is the path to burnout. If my company was dumb enough to have me do the accounts receivable for the month I would not be too disappointed in myself if it did not turn out well. Defining requirements usually just entails describing what you do everyday, so that it can be automated. If none of the employees can do to that, well...the company may be in trouble. Sep 23, 2011 at 13:38
3

In the first place, keep the data normalized (not redundant) as much as you can. If the data is fully normalized, no single update to the data can make it inconsistent.

You can't always keep the data normalized, in other words you may not be able to eliminate redundancy, in which case it can have inconsistent states. The thing to do then is tolerate the inconsistency and repair it periodically with some kind of program that sweeps over it and patches it up.

There is a strong tendency to try to manage redundancy tightly by means of notifications. These are not only difficult to be sure they are correct, but can lead to enormous inefficiencies. (Part of the temptation to write notifications arises because in OOP they are practically encouraged.)

In general, anything that depends on time-sequence of events, messages, etc., is going to be vulnerable and require tons of defensive coding. Events and messages are characteristic of data with redundancy, because they are communicating changes from one part to another, trying to prevent inconsistency.

As I said, if you must have redundancy (and chances are pretty good you must), it is best to be able to a) tolerate, and b) repair it. If you try to prevent inconsistency solely by means of messages, notifications, triggers, etc., you will find it very hard to make it robust.

3
  • write for reuse.
  • write tests. trivial, nontrivial, some absurdly complex ones to see how it handles under such conditions. tests will also help you determine the form of the interface.
  • write the program to fail hard (e.g. assertion). my code has a ton of reuse and i test for a ton of cases - there's more error checking/handling than actual implementation (based on line count).
  • reuse.
  • immediately fix things that go wrong.
  • learn and build from experience.

errors will come up along the way, but they will (fortunately) be localized and they will (in most cases) show up very early in testing. the other benefit of reuse is that the client/caller can save most of the error checking/scaffolding using that which is brought by the implmentation.

your tests shall then define your program's capabilities and how robust they are - keep adding tests until you are satisfied with the success rates and inputs; improving, extending, and fortifying as needed.

2

I make this distinction by writing code with well-defined, but not necessarily optimal behaviour for very unlikely execution passes. For example, when I am quite sure (proven, but not tested) that a matrix will be positive definite, I insert an assertion or exception into the program to test the state, but do not write an own code path for it. Thereby, the behaviour is defined, but suboptimal.

2

Robustness: The degree to which a system continues to function in the presence of invalid inputs or stressful environmental conditions. (Code Complete 2, p464)

The important question here is to ask how important robustness is to you. If you are Facebook, it is really important that your website continue to function when someone puts special characters in the input, and that your server stay up when 100million users are logged on simultaneously. If you are writing a script to perform a common operation that only you do, you don't care much. In between there are lots of levels. Making a judgement as to how much robustness you need is one of the important skills a developer should learn.

The principle of YAGNI applies to adding features that a program might need. But that principle doesn't apply to robustness. Programmers tend to overestimate the likelihood that a given future extension will be needed (especially if its a cool one) but they underestimate the likelihood of things going wrong. Also, if it turns out that an omitted feature is needed after, the programmer can write it later. If it turns out an omitted error check is needed after all, the damage may be done.

Therefore it is actually better to err on the side of doing the checks for unusual error conditions. But there is a balance. Some of the things to consider in this balance:

  • How often might this error occur?
  • What is the cost of having this error happen?
  • Is this for internal or external use?

Don't forget that people can - and will - try to use your program in unexpected ways. It's better if something predictable happens when they do.

As a last line of defense, use the assert or the shutdown. If something happens which you can't work out how to deal with it, shut down the program. That is usually better than allowing the program to go on and do something unpredictable.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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