My refactoring method is to do a copying garbage collection where I open a new file and copy the good functions across, or good portions of functions. It’s like a mini rewrite that is much quicker. I find it kills lots of bad code.
I’m skeptical that we, as programmers, have left ourselves enough information to correctly do this. We typically underspecify things, and as a result, I feel we’ll miss edge cases more often. But, that’s a feeling not backed with evidence…
This is where TDD (or something like it) helps: if you don’t have a test that checks for the behaviour you want then you don’t get to count on it.
TDD is often tied to specific methods/functions, which means they’ll need to change as well as well if you reshape it. If you keep the interface the same, then tests will be fine, provided they specify every edge case appropriately.
I’m personally really interested in the idea of writing contracts for every function, and property testing the integration points (defined as a place in which one function calls possibly many more untrusted functions). That way, provided you keep the contracts the same, or even if you change them really, the rewritten code still has to satisfy the contract, giving you some hope that things will still work. Of course, the contracts can be wrong, or under specified, same as tests, but at least you’ve invested only in defining the properties, not the boilerplate of test setup.
I’m trying to come up with a good way to test out this theory. My thinking, currently, is to define a set of small problems, write an implementation, and typical unit tests, getting as close to 100% coverage as possible. Then do mutation testing to figure out how effective the suite was.
I’ll then implement the same functionality, with contracts, and minimal unit tests (PBT, using post conditions as the properties, essentially), and compare the results of the same mutations. My hypothesis is that the contracts version ends up being significantly less code, but is as effective, if not more than the 100% coverage unit tests.
If you have thoughts of simple problems to test with, or see obvious flaws that I’m missing, please let me know.
Bertrand Meyer describes this in his discussion of the Open-Closed Principle in OOSC, either edition. However he goes further (here, I’m paraphrasing the discussion from the second edition):
Given a module A, used by modules B,C,D that has a method you would like to change:
With the risk that, if the module A’ violates the Liskov substitution principle, it might break, C, D and E in much more subtle ways. That means, the new method must preserve all the invariants and all observable behaviors of the original as expected by C, D and E. And the assumptions in their programmers’ mind.
In a few cases, I survived the mess thanks to cut&paste: a new B’ uses a brand new A’.
Also, in the context of domain driven design, this is likely to be the only correct approach: you have to change A’s method not to fix B, but because B’s use have changed after a change in business rules. It’s better to create a new bounded context were B’ and A’ are terms from the language the domain expert uses that have nothing to do with the original A and B except the word.
OOSC describes Eiffel; Eiffel has design-by-contract; subclassing A means creating a class that inherits the invariants (and preconditions and postconditions) from A. That (along with Eiffel’s uniformity of reference) is why I said “inheriting the features you need to not change”, rather than the methods.
I should point out that in Eiffel you cannot violate the LSP in a subclass that was designed by contract. It enforces the requirement that a subtype cannot tighten preconditions or loosen postconditions, so your subtype will work anywhere the parent type worked.
For whatever reason, I write a lot of switch cases. So practically speaking, I’d end up replacing lots of code with arrays of function pointers and building jump tables by hand to reduce retyping. I think I’d prefer to let the compiler handle that.
Rubyists nowadays are extreme in their methodologies, patterns and coding styles. Never edit methods, never chain method calls, no more than 10 lines per method, remove duplications by all means. This frightens me, once fun “hipster” language turns into high modernist language for people who always do things right (and there is the one right way to do things).
I guess it would solve the problem where your only documentation of the business process IS the code.