(And what I do when I realize I've made them.)
I'm calling these "mistakes", but that's maybe the wrong word. It's often wise to build something simple and only complicate it when we have proof that we need to do more. Sometimes, these things look like mistakes when we reflect back, but they were the strategically correct decisions to make at the time.
There is some important logic that exists in three or more places. The repeated logic represents the same real world concept so if it's changed in one place, the other places must also change. This does not apply to every instance of duplicated code; sometimes different business rules end up being encoded in the same way even though they represent different ideas and they change independently. This mistake relates to duplication of code where all duplicated instances must change if any of them are changed.
I optimize for short-term productivity by choosing to copy+paste insted of extracting something reusable. Although it can be wise to copy+paste the first time I reuse something if I'm not entirely sure how often I would reuse it or what interface it should have, the cost-benefit changes when I copy+paste for the 3rd, 4th, 5th+ time.
I want to change something, but I'll have to make the change in several places. It was supposed to be an easy change but it's looking like it'll be pretty complicated.
When I notice I've made the mistake, I extract a reusable component:
git commitafter each step
git resetback to the last good step and try again (and maybe try a smaller step this time)
A unit of code (file, class, etc.) handles so many different cases/situations that a human mind can't easily predict how the thing will behave in a certain situation.
I optimize for short-term productivity by choosing to add new stuff to an existing thing instead of making a second thing. It takes less imagination to jam more logic into an existing thing than to create a second thing.
I need to change the Big Thing but...
When I notice I've made the mistake, I decompose the Big Thing. I got into this situation because I skipped the imagination step. It's time to reflect about what is this Big Thing? I start to identify sensible chunks of it, and extract them one by one (with a full verification and commit step after each extraction). Once I'm feeling like the Big Thing is just an appropriately sized thing, then I proceed with the change that started this whole investigation. That change should be a satisfyingly small git diff because the Big Thing is now many small, well factored things that are easy to change in the ways that I need to change them.
One real world concept is modelled by three or more different units (classes, files, etc.). Any time I want to understand or change the model for that real world thing, I'll have to understand all of those 3+ components and how they interact.
I tried to make something "easy to change" without getting specific about what sorts of changes I would have to make. I didn't use TDD or domain modelling to understand the natural or appropriate boundaries around logic; I coded and split stuff up often (becuse big is bad) but I didn't do much critical thinking about what is a suitable place to split things up. I could have started with everything together and split things apart after learning from the prototype. Or, I could have carefully modelled the business domain that I'm working in so that I've put things that change together in one place and things that change independently in different places. (That's a hard task, because it requires guessing at what changes we'll need to make in the future -- we rarely have accurate foresight about that.)
When I go to make a change, it feels like I'm reverse engineering a chain or tree of dependencies between components. I think to myself: "why do I have to touch so many files to make this simple change?"
First, I ask myself if the change I'm making is an uncommon change. If I don't expect that I or anyone else will have to make this change again for one or more years, then it's probably OK that the change is complicated, and I'll leave the working system alone. However, if I'm making a common change that I or others will be repeating regularly moving forward, then I'd like to make it easier to make this sort of change.
I find it's much harder to recover from having the wrong abstractions than from having a Big Thing that needs splitting. When everything is jammed into one Big Thing, it's obvious that there's a problem and it doesn't take much reverse engineering to put the pieces together. On the other hand, when the thing is Too Spread Out, we tend to ignore it for longer. The units are small; it appears to be well factored. The problem goes unnoticed for longer, and at that point the recovery is more expensive (sometimes 2-5 days of testing and refactoring).
Something to add? Continue the conversation on Mastodon