Take the 2-minute tour ×
Programmers Stack Exchange is a question and answer site for professional programmers interested in conceptual questions about software development. It's 100% free, no registration required.

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.

share|improve this question
2  
Code it in Agda2 –  SK-logic Sep 23 '11 at 13:52
    
a concrete example would greatly help making your point. :) –  João Portela Sep 23 '11 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. –  DJClayworth Sep 23 '11 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 '11 at 22:33
    
I think you do brother. Amen to that ;) –  user124864 Mar 27 at 15:16

8 Answers 8

up vote 31 down vote accepted

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.

share|improve this answer
19  
+1 for Simplicity is robust, complexity is fragile –  maple_shaft Sep 23 '11 at 12:38
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 '11 at 13:08
5  
This is Sparta!!! –  Tom Squires Sep 23 '11 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. –  PeterAllenWebb Sep 23 '11 at 17:06
4  
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. –  DJClayworth Sep 23 '11 at 20:43

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.

share|improve this answer

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.

share|improve this answer
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 '11 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. –  Morgan Herlocker Sep 23 '11 at 13:38

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.

share|improve this answer

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.

share|improve this answer

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.

share|improve this answer

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.

share|improve this answer
  • 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.

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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