4 ms·
The whiplash I get from reading this article is massive. One second they agree that bad abstraction (filled with conditionals) is bad but then say: > So instea
by joshstrange 3y ago
The whiplash I get from reading this article is massive. One second they agree that bad abstraction (filled with conditionals) is bad but then say:
> So instead of “duplication is cheaper than the wrong abstraction”, I would say “duplication is cheaper than confusing code littered with conditional logic”. But I actually wouldn’t say that, because I don’t believe duplication is cheaper. I think it’s usually much more expensive.
(emphasis on the last sentence)
I couldn't disagree more. In fact it's an incredibly "junior dev" mindset that sees 2 pieces of similar (or _even identical_) code and is compelled to abstract it. Unless there are at a _minimum_ of 3 implementations I think it's always better to duplicate. I've watched too many "common" functions grow over time with way too many arguments, too many conditionals, and way too confusing for anyone to easily follow. The most egregious is different return values based on arguments passed in. I'm not talking "array of strings" or "null" but "array of strings" or "single string" (or worse).
Abstraction can be fun to write and it feels like you are doing something to help "future proof" (also XKCD 927 [0]) but in reality it boxes people in (especially if you try to abstract with less than 3 real implementations) and leads to overly complicated code, or worse "clever" code.
As I've grown as a dev I'm less and less inclined to write "magic" or highly abstracted code and prefer dealing with"boilerplate" that I can tweak as needed for the individual use-case. Only once I have a clear pattern of code that's been deployed and used for a good bit of time do I reach for abstraction/reusable code.
[0] https://xkcd.com/927/ https://xkcd.com/927/
- jamil7 3y ago> I couldn't disagree more. In fact it's an incredibly "junior dev" mindset that sees 2 pieces of similar (or _even identical_) code and is compelled to abstract it. Unless there are at a _minimum_ of 3 implementations I think it's always better to duplicate. I've watched too many "common" functions grow over time with way too many arguments, too many conditionals, and way too confusing for anyone to easily follow. The most egregious is different return values based on arguments passed in. I'm not talking "array of strings" or "null" but "array of strings" or "single string" (or worse). I agree with you here and tend to rather, if possible, deduplicate subsystems or sub-functions of similar looking/identical code and keep the duplicate public surfaces.
- joshstrange 3y ago> I agree with you here and tend to rather, if possible, deduplicate subsystems or sub-functions of similar looking/identical code and keep the duplicate public surfaces. Completely agree, take the small parts that are standalone/discrete and abstract them. I greatly prefer something like function1() { commonCode1(); commonCode2(); commonCode4(); } function2() { commonCode2(); commonCode3(); commonCode4(); } Over something like (assume I've inlined the commonCodeX logic): functionCommon(branchingParam) { if(branchingParam) { commonCode1(); } commonCode2(); if(!branchingParam) { commonCode3(); } commonCode4(); }
- mostlylurks 3y ago> I've watched too many "common" functions grow over time with way too many arguments, too many conditionals, and way too confusing for anyone to easily follow. This is not the fault of the abstraction. This is the fault of (especially junior developers) treating abstractions as sacred and non-disposable, which is itself the result of a mindset in which creating abstractions is discouraged. You should almost never modify an abstraction. Don't modify abstractions to cover new use cases, and you more or less won't run into any of these issues. If you need to, create new abstractions and throw old ones away. > Unless there are at a _minimum_ of 3 implementations I think it's always better to duplicate. This is a silly rule to follow, except for the most inexperienced of developers, perhaps. It doesn't take long to gather enough experience to know be able to recognize in most cases whether some instance of duplication is coincidental (structurally similar by happenstance, which could be "abstracted" in a macro-like manner, resulting in something quite fragile to changes) or if you're actually encoding some piece of knowledge into an abstraction. Advice like waiting until a piece of code repeats three times encourages developers to think about abstractions in terms of structural similarity, which is exactly the opposite of how abstraction should be considered.
- joshstrange 3y ago> This is a silly rule to follow, except for the most inexperienced of developers, perhaps. Perhaps you'd consider me inexperienced though I don't consider myself to be so. I've learned enough times that neither I, nor my colleagues, can accurately predict the future and every time we think we know the cases that code will need to handle in the future we guess wrong more often than not. What I'm trying to say is until you are sure a piece of code is literally the same or with tiny differences that you can cleanly abstract you shouldn't try to guess how future code will use the abstraction. It's the same rule of mine where I try to never proactively add functionality to a function/piece of code. You think that you are saving your future self (or peers) time but too many times I've see people guess wrong at what extra functionality we will need and then that code never gets touched and/or gets migrated/updated for years before someone realizes there is no calling-code that uses that functionality but we have been dragging it along this whole time. Could you check everywhere and make sure it's not being used and thus can be removed? Maybe but I understand the desire to make as few changes as possible and preserve the functionality as it was when you first went to edit the code. Overall that's a good idea when making changes and sometimes you don't always know what params all the clients are passing to an endpoint to be sure of if something is still in use or not.
- jkubicek 3y ago> As I've grown as a dev I'm less and less inclined to write "magic" or highly abstracted code and prefer dealing with"boilerplate" that I can tweak as needed for the individual use-case. This is part of creating abstractions to benefit the reader, not the writer of the code. I'm currently refactoring a python package that was designed to make writing ETLs very elegant (it worked!), but as a consequence, when something goes wrong, figuring out what happened involves pouring through 4 different modules, class hierarchies and trying to track variables through multiple layers of abstraction. It's a nightmare for debugging. Simple boilerplate is repetitive and boring, but man would it be so much easier to read
- joshstrange 3y ago> Simple boilerplate is repetitive and boring, but man would it be so much easier to read Yep, and I'll fully admit when I first started out I hated this idea and wanted everything to be super-DRY but I've swung back in the opposite direction (or at least to a good mean). I had a developer ask why we had some boilerplate semi-recently when the function in question was simply calling another function on the parent class, why not just call the parent function directly (it was protected, they wanted to just make it public). I explained that yes, right now we were doing a straight pass-through essentially (this was for a CRUD layer) but that we had learned over and over that over time we needed to add in things like business logic, validation, or data migrations and this way we just needed to change our "intermediate" function instead of adding one later and having to change all the places that were calling the "direct" function. Same idea as with getters/setters, yes you don't "need" them always when you first write them but having those hooks are invaluable down the line.