Refactoring vs. defactoring
understandlegacycode.com
understandlegacycode.com
No one would know what R and C mean, and I'd have to teach that to the whole team. Just use the word "Refactor" if you're refactoring as the first word of the commit, and don't use that word for everything else. You can't just start using your own made up abbreviations without getting buy-in from other people who work on the codebase.
+1. I would say having refactor in commit header is fine too, though it's more natural to start the sentence with refactor.
> With the Refactoring hat , you are not allowed to change the way the code works.
This is the only part I have a small disagreement with. Sometimes, the act of refactoring it _does_ change how the code works, but it's a change in how the code handled ambiguous or extremely complicated states.
Sometimes refactoring results in streamlining an if-statement / configuration-detail mess and it's the right time to fix it.
It may be way, way, way extra work to put all the bugs intentionally back into the code in the refactor, and you may not even put the bugs back in correctly.
More than once, I ended up finding bugs in the old code this way. The ticket said one thing (ticket IDs in commit messages are a godsend), and the code did another thing. There was also a lot of unexpected business logic that was discovered after the first draft of the code, and not documented.
I had the same experience translating text content into calculators. Text can be ambiguous, but code can't. Text had errors that coded logic wouldn't allow.
The behavior is its external behavior, not internal. As a somewhat trivial example, in Common Lisp you may use an association list like:
'((a . b) (c . d) (e . f))
This is grossly inefficient for some operations, but functions as a map. You realize, "Oh, we have alists with, potentially, thousands of elements or have to search it over and over, it's killing our performance". So you replace that with a hash table to get that nice O(1) performance on a lot of your operations. Your users though, never see a behavioral change because they still access the data using the same methods as before: (get-value map 'a)
To them, there has been no change, and consequently for behavioral tests there has also been no change. It performs better (so there is a change, obviously) but it does not impact the guarantees you offered users of the unit.I believe it's actually faster than a hash table for a decently large n, around the order of 50. Obviously this is going to depend quite a lot on how clever your compiler is.
edit: I was off by an order of magnitude, 500 -> 50.
I'm not able to find if any of the free Common Lisps do any kind of CDR coding[1].
Hash tables, though, should be O(1) for retrieval and updates and amortized O(1) for insertions. Alists are O(n) for retrieval and updates and O(1) for insertions (if you don't check for the presence of the key).
Depends on the refactor. At work some people tried to do some automated refactoring with Resharper, which changed names of fields of a class, which broke stuff that relied on them for reflection.
Also persisting stuff in customer DBs that depends on class names (and reflection) is nice source of terror. Shipping a new version of your software with a refactored class names corrupts the whole DB of a customer.
We do that but with XML files, most of our tables are dynamically generated. I'm a bit scared every time I touch this part.
To them the problem must be the way people refactor because it can't be the way things are tested.
- refactoring: one commit
- fixing a bug: one commit
- fixing another unrelated bug: another commit
- new feature: another commit
In old version control systems it might have been difficult to do this, but with git it's a breeze given some practice: even if you're in a middle of something, you can always stop working, do a temporary commit or stash; then checkout main branch, create a new branch; do the thing you want to do (refactoring or bugfix) in isolation; commit; then go back to the other branch, and rebase.
There's also git add -p for making partial commits if you did a lot of changes and want to commit only some of them.
Separating the activities would really ruin my workflow.
The thing is, once you get a habit of committing often (also unfinished WIP work) in small portions, with minimal effort you can then reorder and squash commits and make the final PR really much easier to review _even for yourself_, and twice as much for everyone else
If you're designing new code and you write something, tear it down and bring it back up, but it's never touched the main branch, it doesn't matter how much you do to it.
In my standard development flow, I commit often, but don't PR, and then at the end, I rebase my whole branch on top of main and shrink it down to one-to-few commits, usually one.
Since people are only looking at the commit set at the end of my work in the pull request, that's the only thing that needs to follow the 'refactor vs new code' separation.
Version control makes things so easy I typically always commit the refactoring work to mainline first (code review and all) then fix the bug or repeat with another refactor.
It’s also typically faster, because the refactoring alone is a significantly smaller code review than the refactoring+<change>, and I can keep working on the <change> (i.e. bug fix/feature/more refactoring) while that CR gets looked at.
The word fits well in to TDD's red-green-refactor with the "Transformation Priority Premise". The premise goes as follows: as the tests get more specific, the code becomes more generic. https://en.wikipedia.org/wiki/Transformation_Priority_Premis...
It’s not really a lot of extra work to do that once you get into the habit. Tools like `git add -p` to stage partial changes in files can be useful for separating self-contained refactorings from behaviour changes if you realise they’re happening at the same time and starting to get tangled.
Magit is great for this: it shows an interactive version of `git status` (the "status buffer"), with diffs that can be expanded/contracted for each file and for each hunk; we can also selectively stage only a few lines from a hunk.
I mean, it sounds like an ideal one but I’ve never lived in such a world. Tests always seem to couple to the way code is currently organized, and I can barely imagine any refactoring change that wouldn’t require some changes in my tests.
Your public interface to your lower level modules/classes are the only things you need to directly test. If that's your smallest unit for testing, then a refactor should also (again, in theory) not impact them because the refactor should not change that public interface, at most changing the internal (private) functions and structure which the existing tests can verify just as they verified the unit before.
If you restructure your units (split a unit into two, for instance) then you'll have to change your unit tests and possibly some integration tests if you instantiated the previous unit directly as part of the test. But that's pushing the definition of a refactor.
- This works best if you actually embrace BDD to test for behavior rather than results. The behavior is less likely to change, and along with points below so too are the interfaces under test.
- I find the least test change in statically typed languages where defining types is the first “test”. Behavioral interfaces become much more stable as you define them if you actually know what they are upfront.
- Both of the terms “integration test” and “unit test” discourage this. Validating what your program actually does usually sits somewhere in the middle, and allows you to encapsulate a lot more of what would otherwise likely change in tests.
- Stricter interfaces fare better than looser. That doesn’t mean give up on the Robustness Principle, but it does mean don’t let it become infectious (I now want to blog about the “color” of functions which accept “options” or other miscellany). The sooner you parse/validate/have concrete and reliable types, the less your code under test and the tests themselves will change.
And that, while it’s my last point, is the most important: narrow types early. Get stability inside the seams of your program.
The other choice is to roll the whole thing back or practice forward-fixing. The latter is an entire discipline in itself.
<link data-react-helmet="true" rel="canonical" href="https://understandlegacycode.com/"/>
[0] https://hn.algolia.com/?dateRange=all&page=0&prefix=false&qu...