I think that one of the fundamental principles of software engineering is to code in such a way that future change is contained. In fact, I would say this is one of the key things that differentiates software engineering from programming.
Sometimes that means centralising code so that change can be limited to one location. Sometimes that means creating strong seams between areas so that changes stop rippling out at the boundaries. Sometimes that means allowing duplication strategically so that your inadequately informed preconceived notions about what future change will look like don't precommit you to unnecessary change.
YAGNI and DRY are great, but the fundamental principle underlying them both is the containment of future change.
> I think that one of the fundamental principles of software engineering is to code in such a way that future change is contained
Uff...that's a heavy statement. There is no such hard and fast rule. In fact its mostly the opposite, I argue. 90% of the abstractions don't ever get used. Write twice and then make an abstraction.
I think you're doing software engineering the wrong way, sorry to say.
It's a shame that you're getting downvoted, because I see what you're getting at, but I don't think you're talking about the same things.
I think you mean YAGNI, and I agree. I've seen plenty of over-engineered solutions.
On the other hand, when the parent say "future change is contained," I take that to mean separation of concerns, plugin-based architectures, decoupling of business logic, etc. And I can also say I've seen plenty of under-engineered solutions, where you need to change constants in five places, and twenty different if-blocks just to add a simple feature.
An experienced developer who's been using the codebase a long time can sometimes spot when an abstraction is needed right away. Conversely, an inexperienced developer won't write twice and make an abstraction, they'll write five times and make a headache for everyone.
Last I checked, all businesses continuously modify their codebase. So planning for change seems to be the only way to survive in such an environment. Not sure what your experience is where you’d label this as “wrong.”
When the boss decides they want email forms instead of the beautiful react form you built, because the customer success team thinks clicking links is adding friction, how much of that well planned react code will be reusable?
> 90% of the abstractions don't ever get used. Write twice and then make an abstraction.
Coding to avoid or contain change does not mean overabstracting. Overabstracting generally means more change down the line, which is one of the major reasons its bad.
Here’s the thing. I’m very harsh about criticism of DRY. On the one hand, it does make sense to question your beliefs and to avoid being dogmatic. On the other, it’s very clear that DRY is an absolute prerequisite for any kind of software quality. In fact, it’s the absolute bare minimum. You cannot have quality software with duplicated logic. Not even duplicated twice.
So software engineering is really the art of learning how to properly create DRY code. We should focus more on how to better create DRY code and not talk about the few times where the abstractions we created didn’t work out.
A for loop is a language level construct, not logic right?
Actually for is a great example of why DRY is necessary. Do you implement your own iteration mechanism every time, or do you use the provided tool (for) that your language has generalized for you?
First, a codebase where you have to write every piece of logic by hand every time is much more constrained than an overly-DRY codebase. Let’s make sure that that’s clear.
Then, it’s very difficult to give general advice because the usefulness of an abstraction is completely contextual. But I remove duplication from the start, and I re-duplicate by replacing the abstraction with its code inline in the places it isn’t working any more. That allows me to look for and apply a new abstraction.
Another alternative to DRY is WET: Write Everything Twice. Only make an abstraction on the third code repetition.
I started adopting this a few years ago and it’s surprising how seldom I hit three identical, non-trivial code repeats. Saved a lot of hours over the years not making a seemingly prudent generic layer.
Nothing is more frustrating than having a colleague who never gets things done and are constantly writing abstractions for days. Like bro, you ain't gonna need this - I know you're flexing your abstract methods but it is fucking useless if it will never have more than one overrides.
I don’t understand this argument. It’s trivial to de-duplicate an abstraction by just inlining the code. How is that less efficient than duplicating code? At least in the meantime you get the benefit of shared code changes.
the big issue here is neither DRY nor YAGNI (and they're not solutions either), but this:
"Next week, your PM comes back and says..."
"then your PM says..."
you cannot abstract in code somebody's opinion, at least not other people's. the number of possibilities is too high, and you have no clue what they are. this is why your carefully crafted structure will break down -- it has no basis in reality.
sidenote: if you will call yourself an engineer than you need to think on a different level than implementing somebody's whims. but of course a lot of of this type of work is fiddling with things to make somebody happy. I would think a real engineer would provide a technical solution to a non-technical person for them to fiddle with themselves.
> I would think a real engineer would provide a technical solution to a non-technical person for them to fiddle with themselves.
Fiddling is one thing, and designing a system to enable fiddling is something engineers often do. But a lot of PM requests aren't even close to fiddling. If your product is a streaming video service, adding DVR capabilities is something a PM might request. No amount of non-technical fiddling will enable that functionality. But a well-designed architecture with isolated functional components will make the work of adding such a feature far less painful than a poorly designed one would.
> this is why your carefully crafted structure will break down -- it has no basis in reality.
This is spot on. And also, DRY is for concepts and abstractions, not just duplicated lines of code. Picking the wrong abstractions often has a much higher cost down the line than some duplicated code.
> You had to add styles to every call of GenericButton
Why? You couldn't have used a default style that could be optionally overridden?
> Your GenericButton is starting to swell with edge cases. As time passes, it's going to get worse and worse.
Sure, if you never split these things out into other modules. A bit of composability would fix this.
----
Like, this whole article seems to misinterpret DRY to mean "shove everything physically possible into a single implementation", when nothing can be further from the truth. YAGNI and DRY are not alternatives to one another; they're complements to one another, and both are essential for writing clean, readable, extensible, and maintainable code.
Sure, it's exaggerated to illustrate a point, nobody sane would really write code like that. Yet I've seen plenty of cases of shoving flags & params to existing functions instead of refactoring the code to split things up.
Which is a fair concern, but that's entirely orthogonal to both DRY and YAGNI. Indeed, splitting things up is a key part of DRY, since one of the takeaways is that the split-off functionality might be useful for other things.
For example (provided the language or framework supports this), you could split the styling functionality of a GenericButton off to some separate function (say withStyle) that merges style rules into an element's existing style, and thus instead of having to "change once, fix everywhere" you'd just invoke that function on the ones where you want to override a style. And indeed, if you're using a specific style change frequently, you could split it into its own specific function and reuse it there.
And then - here's the best part - chances are there are things other than buttons that could use styles, so if you implemented withStyle (and derived functions) right - tada! - it's automatically usable with those other things. Wanna style a paragraph? Feed it into withStyle. Wanna style a text input? Feed it into withStyle. Wanna style a picture? Feed it into withStyle.
Unless I'm missing something, it doesn't look like the `active` prop is ever used in those examples. The `disabled` attribute of the button is just statically there in all cases.
As is typical of this type of blog post, it presents a pat story with without considering the trade-offs. Yes, one reason we create reusable abstractions is because we don't want to repeat ourselves. But another reason we do it is so we can have a component that we can write tests for. An another reason is so that the calling code is simpler to understand and work with. If you copy-paste code, you will lose these benefits.
My experience is that junior programmers tend to error on the side of writing lots of repetitive, boilerplate code and they would produce better code by being more willing to write reusable abstractions. It's the mid-level programmers that err on the side of creating grandiose frameworks with unnecessary abstractions.
Personally I like DRY, I like WET, I like YAGNI. I see them all as tools in my toolbox as a programmer and I find the art is to know when to use which tool. In practice, my code never has the perfect amount of abstraction vs. repetition vs. generalization but I think I keep them in a manageable range and refactor when things start to feel like they're going off the rails.
I believe this article isn't much about DRY, but rather abstracting for future use. DRY isn't just doing the inheritance dance footgun.
I'll just add that thinking about future use is a practice inherited from classical engineering (the one without "software"), where you had to plan for the potential future installment of cabling or pipes, before it becomes prohibitively expensive to tear walls down and start anew. No YAGNI ain't gonna save your 19th century self.
If course, this practice doesn't make as much sense to the fast paced start up world, where time to market is more important, RAM is cheap, and your company's going bankrupt in 6 months.
DRY is hard to do right, esp. for inexperienced coders. They see three lines of code in more than one place and extract it into a single procedure, without knowing if those three lines of code present a useful abstraction or they are just necessary boilerplate. Soon the codebase become a rube goldberg device with stuff piping in and out of these buckets of DRY code and run-time switches to activate or deactivate behaviors. Instead, you want helpers and components and tools to be reused but only when you notice a pattern, not before.
The code samples are broken and the examples are oversimplifying the DRY principle. Neither extreme YAGNI, nor extreme DRY are good practices and we all know this for a fact!
I'd say there is nothing to abstract. This level of abstraction is unnecessary and I think the article illustrates the point well. Maybe you'd make a StyledButton, but that's it, the rest is shown to be the wrong thing.
Said Knuth, who was literally writing examples in assembly talking about why decrement loops are slightly faster than increment loops in the same section.
IIRC, that section (maybe the same page) was also talking about loop unrolling and counting the number of cycles used in various situations.
Not big O, literally the cycle count in the MIX architecture.
This quote does a lot damage in modern times because people forget the context it was said.
Donal Knuth said this when talking about programmers decades ago trying to noodle loops and expressions while they were writing them. In the modern era the same optimizations aren't even necessary at all, because the small stuff is done by the compiler.
People think it is a good plan to throw caution to the wind and figure out speed later, which just isn't true. You have to architect for throughput, latency and interactivity from the start and language does matter.
I kind of dislike this quote because performance matters a great deal and it should be a design goal before code is even written imo which I guess would be as “premature” as possible
I don't think it's a critique of writing performant code. I always took it to mean don't optimize your code before you know where the bottlenecks are. For example, I could spend a lot of time on a Boyer Moore string matching algorithm only to find later that my program actually spends very little time searching strings.
I think of that quote as being in the context of the sorts of 'music box' programs people were writing in the 1970's. This little gear also has a cam that operated the ballerina's arm. It's the sort of thing you do when you need to stuff a FORTRAN compiler in 32k of memory.
If you write your code right then refactoring it to use a different more whatever algorithm should be if not easy at least 'not hard'.
> I could spend a lot of time on a Boyer Moore string matching algorithm
Could you?
It's like an hour to copy/paste a reference implementation and set up unit tests. Maybe an hour or two fine tuning the implementation for your language and benchmarking some use cases.
I'd been using the same C# Boyer-Moore impl for like 15 years, and happened to have just recently updated it for Span support. I doubt I have 8 hours into it in total, and it's thoroughly benchmarked and tested.
It's one thing to design a program such that it can be fast, and quite another to focus on some random piece of code and spend weeks optimizing it, only to find out later that the performance of that particular piece of code was irrelevant.
Sometimes that means centralising code so that change can be limited to one location. Sometimes that means creating strong seams between areas so that changes stop rippling out at the boundaries. Sometimes that means allowing duplication strategically so that your inadequately informed preconceived notions about what future change will look like don't precommit you to unnecessary change.
YAGNI and DRY are great, but the fundamental principle underlying them both is the containment of future change.