The typical programming designing method that I’ve come to use goes like this:
1. ID the problem and what's required
2. Make your program do something simple
3. Test it
4. Make your program do a little bit more
5. Repeat 2 until all the micro problems are solved by the program as a whole
Basically, incremental design. So, all the pieces can be split apart and each micro problem is solved by a micro problem function; does a great job with cohesion and reduces coupling.
Then, sometime in the future, the program comes across a problem that is just like the problem it is built on, but requires a little extra. So, I’d say add the functionality to the program, to make up for it. If, when you’re adding that functionality, the already established working logic has to be edited to make the new functionality work, it suddenly seems like you’re messing with something you shouldn’t.
So, is there a designing method where every program developed with it can be added onto without compromising working logic?
Example:
word = yes,no,"maybe,so"
oldCSVSplit(word, ",") -> {"yes", "no", ""maybe", "so""}
newCSVSplit(word, ",") -> {"yes", "no", "maybe,so"};
The oldCSVSplit function does what it should, split on “,”. Then, sometime in the future, commas inside quotes must be ignored to properly get entries. Adding the functionality to ignore single/double quotes would severely alter the logic, with more ifs and checks and even a stack for keeping track of starting quotes. Is such a scenario avoidable? What method can make such additions painless and less compromising to working logic?
2
There’s no way to guard against all changes in requirements. No matter how you design your code, there always exists a requirement that’ll force you to alter the functionality. Attempting to foresee all the possible changes is futile and ill-adviced – you’ll end up with over-generalized and over-abstracted code, paying an up-front cost of increased complexity without knowing for sure it’s going to pay off later. Worse, since there’s always a change your code isn’t protected against, there’s no limit to how far you can overengineer your code.
The best you can do is code for the current set of requirements and adapt to change. If you keep your design modular, even if new additions require significant amounts of rework, you’ll at least be fairly confident you’re not inadvertently breaking completely unrelated functionality. Once you get burned by a particular kind of change, then you can make your code resistant to other changes of that same type. (If it happened once, there’s a good chance it’ll happen again.)
So, is there a designing method where every program developed with it can be added onto without compromising working logic?
No. As you refine, you necessarily change existing logic. You hide it, obscure it or re-write it.
Well, I would say that changing software that was written before is a fairly big part of what programming is all about.
I’d wager that most developers are paid to maintain, extend and fix brownfield projects rather than greenfield ones.
If you need to get a program to handle a scenario that it wasn’t designed to handle, you have to change something and shouldn’t feel bad about it.
I might have misunderstood your post but, to me, it sounds like either you don’t practice TDD or, if you do, you got it kind of backwards.
If the first is true, you should by all means look into it. If the second is true, you might want to read about it again.
In case you don’t know what I’m talking about, TDD in a nutshell is:
- You write a test that confirms the software can’t already do what you want it to do
- You execute the test, and it should fail
- Only then do you write some code, as little as possible, to make the test pass
- If the test passes, bam, you’re golden and can refactor the code to make it more understandable/efficient/beautiful. A further run of the test will confirm you didn’t break anything while refactoring
- If the test doesn’t pass, well, tough luck, you’ll have to figure out why. The good news is that, if you do unit testing right, your test will be small and focused enough to make finding the bug a breeze in most cases, without even resorting to the debugger
If you developed your program with this approach, testing that it still splits on commas but not if a comma is between two double quotes is trivial.
You will have old tests that tell you the program behaves as originally intended, plus new tests that tell you that it can handle commas inside double quotes.
Learning more about TDD is left as an exercise for the reader.