Programmers Stack Exchange is a question and answer site for professional programmers interested in conceptual questions about software development. It's 100% free.

Sign up
Here's how it works:
  1. Anybody can ask a question
  2. Anybody can answer
  3. The best answers are voted up and rise to the top

During a discussion, one of my colleagues told that he has some difficulties with his current project while trying to solve bugs. "When I solve one bug, something else stops working elsewhere", he said.

I started to think about how this could happen, but can't figure it out.

  • I have sometimes similar problems when I am too tired/sleepy to do the work correctly and to have an overall view of the part of the code I was working on. Here, the problem seems to be for a few days or weeks, and is not related to the focus of my colleague.
  • I can also imagine this problem arising on a very large project, very badly managed, where teammates don't have any idea of who does what, and what effect on other's work can have a change they are doing. This is not the case here neither: it's a rather small project with only one developer.
  • It can also be an issue with old, badly maintained and never documented codebase, where the only developers who can really imagine the consequences of a change had left the company years ago. Here, the project just started, and the developer doesn't use anyone's codebase.

So what can be the cause of such issue on a fresh, small-size codebase written by a single developer who stays focused on his work?

What may help?

  • Unit tests (there are none)?
  • Proper architecture (I'm pretty sure that the codebase has no architecture at all and was written with no preliminary thinking), requiring the whole refactoring?
  • Pair programming?
  • Something else?
share|improve this question
14  
Ah, the good ol' "cascading waves of failure" design pattern. :-) – Brian Knoblauch Dec 9 '11 at 18:36
1  
I liken it to a bubble in a sheet of contact. Push it down, it pops up elsewhere. The better my coding gets, the less I see it – johnc Dec 13 '11 at 22:44
2  
On a side note, I had exactly that on an embedded system. I added a function call to fix an issue. That function call was too much for the stack (the microcontroller had no stackoverflow detection) and so it wrote some random stuff elsewhere to the memory, which of course broke something somewhere completely different. So, this thing CAN happen on a small codebase with only one developer and good architecture. – risingDarkness Jan 15 at 15:00
    
...and that was a nightmare to debug. – risingDarkness Jan 15 at 15:02
up vote 32 down vote accepted

It doesn't have much to do with focus, project size, documentation, or other process issues. Problems like that are usually a result of excessive coupling in the design, which makes it very difficult to isolate changes.

share|improve this answer
14  
this combined with poor or no regression testing – Ryathal Dec 9 '11 at 17:52
3  
True, @Ryathal, although regression testing won't prevent those kinds of bugs, just let you know about them sooner. – Karl Bielefeldt Dec 9 '11 at 18:03

One of the causes can be tight coupling between the components of your software: if there are no simple, well-defined interfaces between components, then even a small change in one part of the code can introduce unexpected side-effects in other parts of the code.

As an example, lately I was working on a class that implements a GUI component in my application. For weeks new bugs were reported, I fixed them, and new bugs appeared somewhere else. I realised that that class had grown too large, was doing too many things, and many methods depended on other methods being called in the right sequence in order to work properly.

Instead of fixing the latest three bugs, I did some strong refactoring: split the component into a main class plus MVC classes (three additional classes). In this way I had to split the code into smaller, simpler pieces and define clearer interfaces. After the refactoring all the bugs were solved and no new bugs were reported.

share|improve this answer

It's easy for one bug to mask another. Suppose bug "A" results in the wrong function being called to handle input. When bug "A" is is fixed, suddenly the correct function is called, which has never been tested.

share|improve this answer

Tight coupling, lack of testing, these are probably the most common culprits. Basically the issue is just shoddy standards and procedure.

Yet if you work in a very large codebase, I would add one common one which can occur even with looser coupling and greater care to testing procedure.

The Side Effects of Removing Parasites

Codebases evolve against their current state. If there is something malfunctioning that flies under the radar of testing, and it is allowed to stay that way for years, the tendency of the system will be to evolve against that malfunction, turning it into a symbiotic parasite. To remove the parasite would cause further malfunctions all over the place.

As an analogy (but don't take it too literally, as it's not necessarily caused by a confusion of definitions and semantics), consider that someone mistook an apple for a pear. They started referring to pears as "apples". Hand me those "apples" (person responds by grabbing a basket full of pears). The whole community evolved to understand that an "apple" is actually a "pear" and vice versa learning by example, and started to thrive on this malfunction.

Now, a foreigner comes into the community. "Hands me those apples" -- the foreigner fetches a basket full of what are actually apples. "I said hand me apples, not pears." --"These are apples. Those are pears," says the foreigner. "What???" Now the foreigner grabs an encyclopedia, shows the community their malfunction.

In this case, initially the bug appears to be on the foreigner's side. From the community's standpoint, he's the one making the mistake, and his behavior of fetching a basket full of apples when requested to fetch a basket full of apples, in spite of being the actual correct thing to do, is now perceived as a glitch. When the foreigner corrects the community (fixing the root of the problem), years worth of misbehavior now become exposed as the actual glitch.

That's usually how it goes. This is an analogy, it's not necessarily something that can be fixed by renaming an identifier or updating documentation. But this symptom of a bug fix exposing all kinds of new bugs can occur in such circumstances, where the bug fix really addressed the heart of the issue, but in doing so, unraveled all the glitchy behavior of the codebase that grew, for years, to depend on what was wrong to function correctly. If you simply remove a parasite, there can be all kinds of unanticipated side effects.

Coincidence

This is a human phenomena mistaking coincidence for truth. It's built into all of us yielding superstitions. A mystical crystal makes a person diagnosed with a disease recover as a 1 in a billion case. Now mystical crystals are the correct course of action for someone with a deadly illness, surely the person with the disease could not have been misdiagnosed, doctors are perfect. If a person attempts to correct this glitch, they will unravel an entire history of misbehavior, and it will possibly even turn into a debate of who is right and who is wrong.

Programming can be like that against large teams with a long legacy. To correct a root of a problem can challenge all similar code ever written, and unravel a parasite.

Getting Lucky with memcpy Against the Linux Kernel

As a totally concrete example, consider an example from Linus Torvalds where someone in user space was using memcpy in various places for overlapping copies when it's not supposed to do that properly, and even documented as not capable of doing this with a citation to use memmove in those cases. Yet it luckily worked over many years improperly using memcpy until an update to the Linux kernel and C runtime, which was perfectly correct (and still preserving the documented behavior of memcpy and memmove) made that code which just "luckily worked" over many years run out of luck. In that case, fixes and updates exposed previous bugs that were always there, but just getting lucky because they happened to rely on something that was incorrect but luckily didn't cause any malfunction at the time.

Yet it was bound to happen that lady luck would stop smiling on such misbehavior at some point.

This can't always be protected against with testing since if the community wrote the tests, they would hold up a pear to the system, ask if it's an apple, and the system would respond that it's an apple. It's only possible to verify correctness if you know what "correct" is, so the same mistake that caused the mistake in the first place will leak into the test, checking for incorrect behavior with the assumption that it's the correct behavior.

With the memcpy misuse case, the people who wrote the code obviously thought it was correct to use memcpy for overlapping cases. The mistake in even understanding what "correct" means here lead to years of accumulated bugs which silently flew under the radar, until a change was made which revealed all those mistakes made in the past.

share|improve this answer

Well, the immediate cause is two wrongs making a right, or at least making a not-obviously-wrong. One part of the code is compensating for the incorrect behavior of the other part. Or if the first part isn't "wrong" as such, there's some unwritten agreement between the two parts which is being violated when the code is changed.

For example, suppose functions A and B use a zero-based convention for some quantity, so they work together correctly, but C uses one, you might "fix" A to work with C and then discover a problem with B.

The deeper problem is a lack of independent verification of the correctness of the individual parts. Unit tests are designed to address this. They also act as a specification of the proper inputs. E.g. a good set of tests would make it clear that functions A and B expected 0-based input and C 1-based.

Getting specifications right can also be done in other ways, from official documents to good comments in the code, depending on the needs of the project. The key is understanding what each component expects and what it promises, so you can find inconsistencies.

Good architecture helps with the problem of understanding the code, making this easier. Pair programming is helpful for avoiding bugs in the first place, or finding them more quickly.

Hope this helps.

share|improve this answer

It sounds like these "new" bugs are not actually "new" bugs. They were just not a problem, until the other code that was broken, was actually fixed. In other words your colleague does not realize he actually had two bugs the entire time. If the code that is not proving to be broken wasn't broken, it wouldn't have failed, once the other peice of code was actually fixed.

In both cases a better automated test regimen might be helpful. It sounds like your colleague needs to unit test the current code base. In the future regression testing will verify existing code continues to function.

share|improve this answer

Improve the breadth of your automated test regimen. ALWAYS run the full set of tests prior to committing code changes. That way, you'll detect the pernicious effect of your changes.

share|improve this answer

I just encountered this when a test was incorrect. The test checked a given permission state that was !correct. I updated the code and ran the permission test. It worked. Then I ran all tests. All of the other tests that used the checked resource failed. I corrected the test and the permission check, but there was a bit of panic at first.

Inconsistent specifications happen as well. Then it's almost guaranteed that fixing one bug will create another (exciting when that particular part of the spec isn't exercised until later in the project).

share|improve this answer

Imagine that you have a complete product. Then you add something new, everything seems fine, but you broke something else, which depends on some code that you change to make the new feature work. Even if you don't change any code, just add functionality to existing, it might broke something else.

So basically you nearly answered yourself:

  • loose coupling
  • lack of tests

Just learn to adapt TDD principle (at least for new features) and try to test every possible state that can happen.

Pair programming is great, but not always "available" (time, money, both..). But code reviews (by your colleagues for example) once a day/week/set of commits will help greatly too, especially, when the review includes the test suite. (I find it hard to not write bugs to test suites... sometimes I must test the test internally (sanity check) :) ).

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.