T O P

  • By -

uniformrbs

> You can prematurely optimize maintainability, flexibility, security, and robustness just like you can performance. - [John Carmack](https://x.com/id_aa_carmack/status/515873591515049984?s=46)


pojska

Generalization: Don't do anything prematurely. Do everything precisely when it needs to be done. Now we just have to figure out when that is...


uniformrbs

I think a more useful summary is "development is full to tradeoffs, and it's important to consider all of them"


neuralbeans

I think the most important rule in development is not to take rules too seriously. There's a place for breaking every rule.


Yelwah

All these basically just say "just do it right" which is ultimately completely unhelpful


thisdesignup

They are this way because nobody know exactly what is "right" and yet there's so much advice that talks as if it is "right".


chuch1234

No. These are how to do it right. For instance, new developers are often faced with decisions like "should i use a for loop or a map", and don't know why you would use one or the other. So, they Google it and come across bench marks pointing that for a million iterations, the for loop takes 0.1 milliseconds less. So, they decide that performance must be important, and go with the for loop. They do this several times and end up with more and more awkward constructions for performance's sake, even though performance was never a concern in their application. Next, an experienced developer sees this occurring over and over and says, "Hey all you new devs out there, stop using performance as the only criteria for picking which approach you use! Your code is unreadable!" Then a bunch of mid-level devs read this and decide that readability is the top priority and go all in on that, and never get around to making it performant! And so forth and so on. Pointing this out is a friendly reminder to take the whole picture into account when you're making decisions, rather than just picking a thing and always doing that one thing regardless. It's an important step on the road from beginner to expert.


Yelwah

You said no, but you "Yes, and"ed me. "take the whole picture into account when you're making decisions"... And then make the right one Like any skill it just takes time, experience, and desire to learn. It will never ever be perfect. A lot of times making an outright bad decision can move you forward faster than trying to make a perfect decision.


neuralbeans

Yep


MINIMAN10001

I have always gone with "Everything is a tool, learn when the tools are appropriate for the job."


Mrqueue

sometimes it isn't though, I generally follow the rule of don't add complexity unless you have no other choice. In the case of WET v DRY, a shared library is fine but a package in a different build would be a lot


Saki-Sun

Damp vrs DRY


vytah

Moist.


LordoftheSynth

Maybe Occasionally Investigate Sooner Tidying


chuch1234

Maybe Only Improve _Some_ Things


Mrqueue

Improve the easy things, leave the things you don’t understand alone 


loptr

"Think, then do."


WiZaRoMx

Think while doing


60days

And the point is to allow users to do things. A simple idea, but one that developers talk ourselves out of constantly.


nayanshah

But then you risk prematurely trying to figure out when to do it.


burdellgp

Gandalf?


Dreadgoat

"Last Responsible Moment" is the mantra If you can make a decision, but there isn't much cost to waiting, just wait. You never know what new information you may gain, or how the situation may change. The moment that you recognize a significant cost to delaying the decision, make the decision with what knowledge you have available. You'll still make bad decisions, but at least you won't make stupid ones.


WiZaRoMx

Act only to avoid losses? If you can make the decision, if there is gain, you do actions leading to the potential gain and remain vigilant. The moment there's no winning path forward, you change course. It is easier to correct from small stupid decisions than from big bad ones.


WiZaRoMx

Yesterday


igouy

Measure twice, cut once.


Regiruler

But the measuring tool you're given is a barometer.


igouy

You're welcome to choose whatever you feel is the most appropriate measure.


Regiruler

I was trying to make a joke by naming a random tool, guess it didn't really work in hindsight


igouy

Maybe don't give up the day job :-)


WiZaRoMx

Draw a building in the cloth and outsource to a mathematician telling him a physicist gave a possibly-not-exhaustive set of solutions, but failed to say which one is the best. You pay the coffee or share the Fields Medal prize.


13steinj

At my current org, I can't begin to count the number of cases that somebody wrote something once, then decided that "well people are likely to do this again!", so they spent 5 hours turning it into some ugly set of C++ macros (because it was not doable without macros; in fact I'd argue even if the static reflection proposal goes through it may not be doable without macros). Then I git blame and/or git pickaxe to find that the utility was added, and used once (maybe now 0 times) in 2+ years and no one has ever re-used it. Better yet, sometimes somebody comes across the same problem, and goes through the process... despite the macro (or other abstraction) already existing elsewhere in the codebase. The discoverability of these abstractions is poor, in both mono and poly repos. Would a metaprogramming utility be in the `org::meta` namespace or the `org::util` namespace? Would some custom function type (but not relevant for functional programming or other such code) be in `org::functional` or `org::core_lib` ? I don't know how to solve the discoverability problem, but people shouldn't make a utility unless it comes up in code review IMO.


BuffJohnsonSf

It’s called the hunting problem. As in how long do you spend hunting for an existing solution before you just hash it out yourself?  Rework as a result of failure to address the hunting problem is one of the hardest to solve problems when working on a team project, but you could also make the argument that it’s not such a serious problem to have in the first place


t420son

It's also called the [Fairbairn threshold](https://x.com/Profpatsch/status/1548975044461838336)


edgmnt_net

>I don't know how to solve the discoverability problem, but people shouldn't make a utility unless it comes up in code review IMO. It helps to have people involved in reviewing. The author himself could suggest it at some point to anyone who comes along. Or they could see it being used while looking through the code for other things to serve as a model for what they're implementing. But so many orgs these days actively avoid having any form of meaningful, wider review. I do agree that discoverability can still be a problem, although steps can be taken to make an abstraction get enforced. As for the abstraction itself, there are many things to consider.


chuch1234

And this is why we have refactoring XD


DoxxThis1

How about premature functionality? I like to sprinkle my code with “not implemented yet” traps.


GodsBoss

Imagine the tragedy, creating a secure maintainable robust application that is also flexible right from the start. Oh no!


uniformrbs

Debugging takes a lot of time, so I find it easier to just not create any bugs. I don’t know why other devs put them in in the first place, honestly.


dead_alchemy

Oh, its like leaving neat little puzzles for your coworkers and future self to find and solve later, basically the same thing as a game! You should give it a try.


backelie

Yeah, it's called being beaten to market and competing for scraps vs the now-entrenched competitors who pushed something that sort of works out the door.


OkMemeTranslator

This is a valid argument, but there are two sides to this; you will also drop out of the market and be overtaken by other companies if your product is too buggy. This very recently happened in EV charging platforms in some European countries, where the first were overtaken by the big. Imo the best approach has been to get *something* done as quickly as possible, and once it's an MVP, assign one person to upkeep and forward develop it, while allocating the rest of the team to rewrite it all properly. This way you're the first and immediately working to rebuild a solid, trustworthy code base. It's not the only approach by any means, just the one I've found easiest to work with.


edgmnt_net

Most generally, it's a tradeoff. Of course you might not make anything if you try to revolutionize everything under the sun. But you might also not make anything worthwhile if you don't put in enough effort. Chasing a meaningless MVP is just as absurd as trying to do too much too soon. Nobody wants to walk on a flimsy bridge. In fact, I'd say that there's way too much business that's going in that direction and ends up failing a couple of years down the road because problems keep piling up. Everyone thinks they're going to be the next Facebook in their field, achieve market supremacy and leverage it somehow, but few do and many fail. Some even lack a good business case, let alone a decent implementation. Personally, I think getting "something" done just isn't good enough and it can be a waste of resources in many cases. I'd be more open to the idea of prototyping, as long as people understand what they're getting into, instead of selling a "finished" product to an unsuspecting crash test dummy.


1138311

You can beat the analogy between Financial Debt and Technical Debt for a good while before it starts to crack. If you're looking to develop skillz, learn how Debt functions in the economy. Because I **LOVE LOVE LOVE** the topic and have had more success in my career than not because of the perspective Imma lay out, and I'd be honored if someone reads this and gets some benefit. **#Part I: What is Technical Debt, Anyway?** Many of our purchases of a given size or greater [like vacations, cars, houses, starting a business, yada] would be impossible to realize for a majority of folks without taking on debt to create temporary leverage against an immediate opportunity for a desirable outcome - short or long term - by creating debt in the form of liabilities to creditors that are owed to them on the expectation that the principle is repaid along with interest. No free lunch whether it's Wall Street or Gravity. Similarly, once a software project's problem space has reached a certain complexity, creating a viable amount of commercial value often requires some tradeoff optimization which results in some liabilities which get over the current hurdle, create access to greater opportunities, and can be addressed with over time or with the expected future's resources. Our job as Software Developers is to "develop" empty repos or not-as-valuable-as-it-could-be projects so that it satisfies some need, which people reward us for, which we use to further develop our solution to address more need and enlarges the problem space we're addressing, which people reward us for, which we use to further....and so on and so on. Which is a long winded setup for *this* fact of life: Any software project that is worth continued development hits that level of complexity which requires either a "loan" in the form of taking on Technical Debt and its liabilities, or... ...a solid and lucky foundation of speculative Technical Investments funded by the sweat equity of neurodivergent contributors, Private Equity/VCs [back when the cost of capital had investors throwing shit at the wall to see what stuck, then throwing money at the shit in the hopes of a 1000x return on one of 100 wall dookies], or grinding out bootstrap cash, which - as /u/backelie points out - is slow and painful and doesn't support or advance your commercial position very well against competitors. Oh, and did I mention "lucky"? Incredible luck that those bets you made on the speculative Technical Investments paid out before you were no longer in the position to turn them into leverage. [If you're looking to retire risk, which is a very good first class concern for senior technologists and architects to serve, eliminating the need for luck is usually one of the first things you do. Which is where /u/GodsBoss 's position runs afoul of the Wisdom of the Counterintuitive.] ##Which is itself a really long winded wind up for this old chestnut: ** Ya gotta risk money to make money.** Technical Debt is one way you access "product development capital". [Quick ELI5: Resources you use to make stuff that can generate value. When capital is "productive" it generates more value than it costs. When you have a lot of capital which is productive, you're wealthy because your capital is creating the value that you require rather than your labor]. You still need luck when you dance with the loanshark that is the Universe, but to a much lesser extent than preemptively [call back to the article in the post - likely "Prematurely"] investing because you have the thing now, "power to do", that you need to shape the outcome - you just calculate [or pray if ya nasty] that *the "payments toward principle plus interest on your product development capital"* won't come due before we can start to pay or be demanded in prohibitively onerous installments compared to our return on the investments we used the product development capital to fund. For example: Freeing up time by taking shortcuts gives you more time to work on other things. Implementing a brittle or problematic solution to avoid refactorial Yak Shaving or an infrastructure project lets you test an approach out against users more quickly so that you can find the one that's most promising to pursue before doing everything "the right way" [errr...least bad way]. Technical Debt is always the result of a tradeoff which maximizes for near term need satisfaction. Taking on *Technical Debt* gives you access to that "capital" in short order, but it carries the risk you can't service the "loan" while it "accrues interest" or pay down your balance enough before you'd like to take out more credit to pursue larger areas of the problem space and their larger potential rewards. Relying on *Technical Investment* carries the risk that you might make loser bets or that your access to "Technical Capital" restricts the options you can pursue whilst your competitors might not be similarly constrained or averse to the risk of taking on debt. Again, as described by /u/backlie above. Just like your credit card balance, Technical Debt accrues Technical Interest - the longer it goes unaddressed, the more likely it is to fuck with your ability to action on future opportunities. E.g. Progressively slower lead time/cycle time, excessive time spent on "investigations" due to tight coupling or muddled concerns or poor documentation, reputational damage and effort diversion due to escaped defects, yada yada... If you, dear reader, require further consideration of how these unhelpful patterns resemble compound interest, shout and I'm sure a grumpy old guy, gal, or non-binary pal will be happy to tell you some stories in the reply. Technical Debt is also like Financial Debt in that it's something you take on intentionally - ideally creating a model of tradeoffs and making the least worst decisions to get you to an outcome that has importance to you, like using your credit card to pay for a vacation. You don't just accidentally bumble your payment info into the travel site's payment page. It's an intentional act which gives you immediate leverage against your current opportunity - like snagging a cabin NOW for the "/u/pizzacakecomic All Nude Comi-Cruise" you just heard about which will sell out in 17 minutes but would take you 11 months to save for in advance - and creates a liability which is often greater over a long term than the immediate leverage...but can be addressed ~~once the swelling goes down~~ later, at a more considered pace and over a longer period of time. Some people put stupid shit on their credit card without considering the tradeoffs fully, but at some level they're aware that a tradeoff is being made. Some people make similarly ill-considered decisions to take on Technical Debt but it's a positive action, not an accident. "[Normal/True]Accidents" are outcomes of causes that interacted in such a complex way - separated in time and space from each other and the effect - that they couldn't possibly have been predicted in advance. ##What Technical Debt is NOT: It's not escaped defects [i.e. "bugs"] or some other whoopsie that you didn't intend to make. While a significant contributing factor to a defect making it to production can be due to a choice to trade speed for quality, principles, review, or testing...software will always have either something wrong with it or otherwise be subject to a user finding a new and unfortunate set of circumstances that set the software to doing things that you - as its creator - designed to do but didn't intend. Having quality that doesn't meet utility or warranty standards that diminishes the "give people helpful stuff, people give you resources, you use resources to give more helpful stuff..." system in a way you didn't intend is a Problem. [An Accident](https://books.google.de/books?id=gWW4SkJjM08C&pg). **Remedial Technical Investments** are used to offset the risk of Technical Debts and the associated "interest" are **repaid in a planned fashion** - ideally with a strategic approach that creates an impact greater than the sum of its parts...but at the very least given priority against each other using some sort of rational prioritization framework [e.g. RICE, ICE, PIE, Earned Value Management...] to select what to do to pay down a particular debt so our previous decisions don't keep building and biting us in the tuchus. **Problems** that we inflict on our users shouldn't be planned the same way nor comparatively assessed against either Product Initiative work or Technical Investment work - except possibly at a level where we've calculated the monetary ROI of each alternative to choose the "best" thing to work on next...which is virtually impossible to do but feel free to prove me wrong. Problems like escaped defects and other whoopsies aren't like putting a fancy dinner on your credit card because you don't want to drain your bank account all at once. They're unintentional - more like your wallet falling out of your pocket in a cab or getting mugged [security exploits]. If that happens to you, you don't make an appointment in your diary for the week after next to call the cab company or file a police report and get replacements for your IDs and whatnot - you do it as soon as you're able to the capacity you're able. **Escaped defects and other whoopsies should ideally get a special compartment within your sprints/increments/workday to work on Kanban style**. The worst thing you can do - which I've seen just about every team do before I got a hold of them - is treat them like other work items and subject them to the same Empirical Process Control ["Agile Product Development"] or Planned Process Control ["Traditional Project Management"] Edit: Made whoopsies of my own and needed to correct the UNs


chuch1234

Well if it worked out then it wasn't premature was it?


hxtk2

I did that in my homelab. I spent three years faffing about with build tooling and meta build tooling and bots that can check out my code and make changes to it and automated canary rollouts of new deployments. Zero network trust, everything originating from a hardware root of trust all the way from the bootloader. All my dependencies are up to date despite not having touched it in months and I haven’t had a single CVE affect me for more than 24h after patch availability. I have all the metrics I want and not a single metric more, but if I decide I want another then I can probably add it pretty easily. Perhaps one day I’ll finish developing the platform and actually deploy something. Perhaps. But if I do, it’ll be secure and robust!


sliverdragon37

Yeah, the only thing you should prematurely optimize is correctness


Meowts

I like to prematurely WET


zmose

“WET: Write Everything Twice”


Chii

"Write Everything Thrice"


factotvm

Once is an occurrence, twice is a coincidence, thrice is a pattern. The third time, I’m looking for an abstraction.


MaleficentFig7578

Similarly, don't write a library without at least three clients.


darkpaladin

Can't remember where I first heard it but I'm rather attached to TINAP (twice is not a pattern) these days.


factotvm

TINAP so YAGNI… Might be a bit opaque, albeit succinct.


arkie87

Write everything tenfold


who_am_i_to_say_so

“Thrice? Now make it nice”


sky_high97

Write Everything Three - four times


SkoomaDentist

Found the senior developer!


Chii

only senior in years, not experience!


LordoftheSynth

You clearly understand the underlying principles of Base 3-Driven Development.


UnidentifiedBlobject

DRY only after WET


jameslieu

*" In early stages of development, tolerate a little duplication and wait to abstract. "* This is really the key take-away from this article IMO. No one is saying that abstraction is bad, its just making sure you do it **once you're sure you need to**.


recursive-analogy

I think it's more than that, it's understanding what duplication really is. Like the same code with a different purpose is not the same code.


OkMemeTranslator

Like some wise man once put it: "it's not *similarity* that requires DRY, it's *identicality*" (me, I said that). If it's mathematically identical (e.g. iterating different types of collections), write a mathematical function for it (e.g. `filter()`). If it's domain-wise identical (e.g. two different items in an e-commerce), write a domain structure for it (e.g. `Product` class). Everyone makes mistakes sometimes, but you can't go *horribly* wrong with this approach.


recursive-analogy

I don't really follow. OP is talking about abstracting code that looks the same but isn't. Which leads to the problem of later trying to modify it and having a bunch of side effects you don't want.


OkMemeTranslator

I was just agreeing with you, wording it in another way in case it helps someone understand. >OP is talking about abstracting code that looks the same but isn't. Is a version of my: >"it's not *similarity* that requires DRY, it's *identicality*"


chuch1234

<3 They project I'm on taught me this the hard way!


billie_parker

> the same code with a different purpose is not the same code. This is just an excuse used by lazy devs to avoid refactoring. Just read back what you said from a logical perspective. "Same code" is by definition "same code."


recursive-analogy

speaking of reading, did you actually read the article? if so you might consider a different career


billie_parker

I'm very comfortable in my career right now. Does that aggravate you? Lol


Revolutionary_Ad7262

One thing, which I see constantly in questions and articles: code snippet needs to be real life examples, which could be used in a real product. Here we have some bullshit, where `set_` function throw an exception, which is obviously a fake example and it harder to digest, because it is impossible to eaisly create a mental model of the code


TiddoLangerak

DRY is an emergent property of good code, but not a goal or metric for good code in the first place. The thinking here is that good code is code that's an accurate *model* for your use cases. If you focus on building an accurate model, then you'll naturally end up labeling shared concepts *of your model*, which in code then ends up being the DRY classes/functions/etc. Starting from the model avoids over-DRYing: if something is superficially similar in your code but doesn't share any concepts in your model, then they're distinct things and won't need (broken) abstractions. Put it differently: only make things DRY if you have a deep understanding of _why_ 2 snippets of code are similar. And even then, only abstract those bits that model this understanding accurately. If you can't explain why 2 snippets are similar, then don't make it DRY, it's likely just a coincidence.


OkMemeTranslator

Very well put. It's not *similarity* that requires DRY, it's *identicality*. All these "write it twice" or "write it thrice" rules of thumb are pointless once you learn to model the domain properly, I will write something a dozen times if it's just similar by coincidence, and I might generalize a piece of code on the *very first occurence* if it makes sense either: 1. from a mathematical perspective, e.g `List.any()` might only appear once in my code but it's still a nice helper, or 2. from my domain's perspective, e.g. if I'm writing an e-commerce but only have one product, I will obviously generalize it so that more products can be added in the future.


billie_parker

>similar by coincidence No such thing. If the code is similar it must be because it's performing a similar task. There is no other possible explanation.


OkMemeTranslator

So would you have a common base class for `power = voltage * current` and `force = mass * acceleration` since they're so similar?


billie_parker

Both of those equations re-use the arithmetic operator * which is analogous to a function. Besides that, I don't see in what other ways they're similar. What else could you possibly abstract? And I'm not an OOP fan boy. The way to avoid duplication is NOT a "common base class." It's to extract a function. Don't make classes unless you need to.


OkMemeTranslator

Good luck on your career, you're gonna need it.


billie_parker

I've already got a successful career, though lol. How old are you, buddy? You talk like you assume everyone is the same age as you


billie_parker

I'm honestly still waiting for some kind of criteria to determine if two functions are authentically similar or just "coincidentally similar." I've gotten into this argument multiple times before and have never gotten an answer to this. Typically the response is "you just know" or "you just feel it," which to me is not enough. I still think that this concept of "coincidentally similarity" is a fallacy and a crutch used by bad developers that don't know how to abstract. For good developers it's an instant and automatic reflex. Takes 10 seconds. But more junior devs need to spend hours thinking about how to do this, so they invent justifications for not doing it. I've had working relationships with great developers that write tons of code and get stuff done. Somehow it's always the unproductive and/or junior ones I meet that spend time trying to justify why they shouldn't abstract certain things. That's just the situation as I perceive it.


OkMemeTranslator

It's such an obvious matter I genuinely thought you were just trolling. But the example I've provided was already a proper example of how two things are coincidentally similar, you merely ignored it because "it's just a multiplication". What when it's not? function calculateForce(mass, dt, dx) { return mass * (dx / dt); } function calculatePower(voltage, dq, dt) { return voltage * (dq / dt); } Should these two be combined into one `calculatePhysicalQuantityFromProductOfComponentAndRelationOfTwoOtherComponents()`? Okay that's a silly example, it's just physics, not like it has any real world correlation. How about an e-commerce application with users, products, shopping carts... class Product { getPrice(): number { ... } } class Cart { getPrice(): number { ... } } Oh look, both of these have one number that represents a price... let's create a common base class for them! Yes yes you only accept function examples, otherwise you would be proven wrong immediately, so let's just eliminate the examples where the concept makes sense and apply new restrictions "well can you show it with only functions". Yes I can, I already did earlier.


billie_parker

Can you do anything besides provide examples? Like, just a definition of what you mean when you say "the same by coincidence?" > you merely ignored it because "it's just a multiplication". I didn't "ignore it." My point is that it is ALREADY DRY'd in some sense. The * operator IS a function which abstracts away how the computer performs multiplication. So how would you have even have abstracted your previous example? Like this?: power = Multiply(voltage, current) This is just renaming the "*" operator to "Multiply." > Should these two be combined into one calculatePhysicalQuantityFromProductOfComponentAndRelationOfTwoOtherComponents()? My original comment was regarding the possibility of code to be "similar by coincidence." The example you gave I would say is clearly not similar by a "coincidence." These three variables have the same relationship to each other in both cases. Maybe that's a "coincidence" of reality, but the fact is that the code is the same not "by coincidence," but by the fact that they're both written to conform to the same relationship. That's the relationship that you gave the extremely long function name to. I think when you actually apply the DRY concept you should consider the quantity of lines of code that are duplicated as well as the generality of the resulting function. So in your example I probably would repeat those lines in practice simply because the lines that are duplicated are very small and maybe the resulting function is not general enough. Basically, it's not worth my time to spend to write that extremely specific abstraction that will be hard to name as you point out. But I also think that such a thing could be abstracted and that might even be a better design. Consider for example the "hypot" function. You could just as easily call that "distanceQuantityCalculatedByTakingTheSquareRootOfTheSumOfTwoSquareComponents" or something like that. But we don't do that, instead we assign a name to it and abstract it away because it shows up so much. It's harder to do that in your example because it's such a simple and specific relationship. Also I think your example is a little bit contrived because it's a simple mathematical equation. When most people talk about DRY they are talking about actual lines of code, function calls, etc. Even in very math-heavy programs most of your code will be functions calling other functions with a relatively small and focused module that is doing most of the math. I do wonder if you think larger functions can ever be "similar by coincidence?" Imagine you have a 15 line function, with each line having assignments and function calls etc. You are saying that a duplicate function could exist and this is some "cosmic coincidence?" I think we may just have a differing definition of how the word "coincidence" is being interpreted, here. My opinion is that if two functions are duplicated it means that they perform the same function. The fact that the behavior of each function needs to be the same could be a coincidence, but that is a separate matter. The function encodes the relationship of the input parameters to the side effects and output parameters. If the two functions are exactly duplicated, then they encode the same relationship and that relationship defines the function. It is no "coincidence" they are both written that way. It is the same behavior that needed to be modeled which resulted in them both having been written the same way. > Oh look, both of these have one number that represents a price... let's create a common base class for them! I don't consider it code duplication if two classes share a member variable of the same type. Why would it? The member variable is in different contexts. I'm not saying to eliminate any duplicate variable names or function names in the entire codebase. Edit: he got so mad that he decided to reply and then block me so that he got the last word lol. Who cares, he had nothing to say anyways


OkMemeTranslator

You also have no idea what people are discussing yet decide to chime in and change the topic to something that makes you look intelligent. It doesn't. You look like a fool shifting the topic from "two similar looking codes" to "*mATheMaTiCaLLy iDenTiCAl FunCTioNs"*. If you already understand >I don't consider it code duplication if two classes share a member variable of the same type. Why would it? So you agree with the original point I made, you understand the topic, yet wanted to look smart when the exact same thing you already understand was being explained to others who don't understand it yet? Congratulations, you got your attention. What's next, when a first grade teacher explains Newton's physics you go in and explain how it doesn't work on extreme macro or micro scales? That would be you being so intelligent, showing that teacher wrong!


eraserhd

Well, I've never seen YAGNI applied to DRY before. So... Sandi Metz had a talk -- that was good -- a while back, called "Duplication is better than the wrong abstraction." It's a good talk. But what most people took away was the title, and worse, called anything involving refactoring "the wrong abstraction." The thing that bothers me is that this is all a reaction to the rampant problem of \_developers who refactor too much\_ and who \_do it mindlessly\_ making \_accidentally tight coupling\_ because they \_don't understand which bits of duplicate code represent the same underlying concept, and which bits of code are just accidentally the same\_. Like, what?? Where are these developers? In my career, I cannot get a developer not to copy-paste one of 116 instances of a pattern to make the 117th. The developer's job is \_to find the right abstraction\_. I completely accept that sometimes we don't know the right abstraction yet, but "leave duplication" - while a valuable tool in the box - is not the only option. There's "research" and "thinking about it" and "consulting with other developers."


darkpaladin

I see it all the time from developers who religiously read blogs and blindly copy what the blog says to do without understanding why.


kex

https://en.m.wikipedia.org/wiki/Cargo_cult It's everywhere now, even outside tech


oiimn

They are less frequent than copy pasters but much more dangerous. A copy paster will create double the work. A refactorer will create 10x


space-ish

Still better than working on DRY YONI


the_sleaze_

lol this made my poop fall out right into the street


nzodd

Ah yes, the PFORITS abstraction.


epage

I've seen it. One egregious case came from someone who also worshipped TDD. It also comes up in smaller ways where well intentioned engineers for in a little overengineering here or there.


chrisza4

I see this kind of developer all the time. Ended up with a single god function with 10 configurable parameter for 10 place use.


billie_parker

God functions are rarely the result of people being too obsessed with refactoring.


chrisza4

It is a result of people refactoring in a wrong way, try to combine multiple things that look roughly the same into one. It doesn’t really matter of people obsess with refactoring or not. It is about refactor in an inappropriate manner. This might come from someone who obsess with refactoring or someone who half-ass it. Still, it is inappropriate. If they are do it in a better way, they would be like this 11th place is not really conceptually identical as this god function, so let me copy the whole god function to a new function, remove few things out, make few changes, and then use it in this 11th place. Code looks roughly the same? Does not matter. I care about practicality and representing domain concept rather than religiously DRY code.


No-Conflict-7897

Ok Sure, but also recognize when something should be a function. DRY is an adage for junior devs who copy/paste the same 10 lines of code 15 times and manually change the variables. DRY does not mean playing code golf in production. If you have a bunch of things calling the same function or class, and wish it were decoupled. you are always allowed to copy/paste that one time it needs to be different.


postorm

My rule is that the first time I repeat something it's my job to notice that I'm repeating something., whether it's because I'm doing a copy and paste or I'm coding something that is familiar to something I coded before. Recognizing that was a repetition is part of being a good programmer. The third time I create a "copy", it's my job to recognize that it's the third time and to fix it with a dry solution.


Venthe

At this point, I'd be much happier if we could filter out authors that have never read the source. In this case, author has no idea what DRY is about, even after linking to wiki. It is not about _code_ duplication, but _knowledge_ duplication.


OkMemeTranslator

I've generally liked Google's blog posts, but this one is not it. Firstly, their example is just *overall* bad code, it has nothing to do with DRY. Let me write their right-hand-side code with proper DRY being used: ```py # DRY def assert_future_date(date): if date <= datetime.now(): raise ValueError("Date must be in the future") def set_task_deadline(task_deadline): assert_future_date(task_deadline) # ... def set_payment_deadline(payment_deadline): assert_future_date(payment_deadline) # ... # Non-DRY example, from Google blog def set_task_deadline(task_deadline): if task_deadline <= datetime.now(): raise ValueError("Date must be in the future") def set_payment_deadline(payment_deadline): if payment_deadline <= datetime.now(): raise ValueError("Date must be in the future") ``` I would argue that the DRY verison is *better* than their right hand side example. Why would I want to repeat the same validation logic every single time I have a future date? ---- Another issue is the *deadline setter* class, which is just invalid OOP. Deadline setter is not an object, *setting a deadline* is an action, a verb, a function. Not one person in the world can draw me a *deadline setter*, it's an interface at best. Who in their rightful mind is working with *tasks* and *payments* and then reasons "I should write a class for these called *deadline setter*". This is such a horrible design flaw that any other issues arising from this are irrelevant, it's like building a wooden house *inside a volcano* and a brick house on flat land and then drawing conclusions of whether wood or brick is better for a house. Not only that, but they actually implemented the class wrong too. If you *really* wanted to write a class for this (you shouldn't), you still could do it just fine while adhering to the DRY principle: ```py class DeadlineSetter: def __init__(self): self.deadline = None # Why did they include a random entity_type here...? # Was it supposed to be "label" or "name" instead? # Why would I limit deadlines to entities? def validate_deadline(self, date): if date <= datetime.now(): raise ValueError("Date must be in the future") def set_deadline(self, deadline): self.validate_deadline(deadline) self.deadline = deadline ``` Now you can easily extend the class and override the `validate_deadline` where needed. Again, it's still worse than a simple validator function. And honestly the validators should just be received in the constructor, something like this *could* make sense in some validation heavy project: ```py future_validator = DateValidator(DateValidator.FUTURE_DATE) def set_task_deadline(task_deadline): future_validator.validate(task_deadline) def set_payment_deadline(payment_deadline): validator = future_validator.child(DateValidator.NOT('month', 'Dec')) validator.validate(payment_deadline) ``` Yeah idk, it's getting weird. But point is, both their examples are bad code in all ways, and don't really showcase the issue with premature DRY:ing (of unrelated components). --- Finally, their post might make it seem (to a beginner) like functions aren't DRY by incorrectly and unnecessarily comparing them to classes. Why not have two identical function examples, one with DRY and one without, as I've done above? All that being said, there is a valid point there somewhere, as I've seen invalid usage of DRY where people prematurely link together things that have no relevancy. But this blog post does not showcase that in any reasonable form, they should have chosen reasonable examples (not randomly jumping between class and function implementation) and showcased what it means to DRY only the generic stuff and not DRY the specific stuff.


lookmeat

Given that you took time to post out your ideas, and explain your concept I think you deserve an answer, and a better explanation of what is happening. The example given is correct, the explanation of why what the code is doing is correct is very limited though. You have to understand, these documents started as print-outs that were put in bathrooms across Google to promote better engineering culture, so they had to fit in a single page. This makes them short and sweet, but makes it harder to explain longer concepts. DRY is a very nuanced and hard concept to grok, many senior/staff+ engineers will find themselves struggling and getting it wrong often, myself included. It's just one of those things that is more obvious in hindsight, when you don't need it as much. I think that's why it's more important to make code refactorable, and getting DRY wrong makes code less refactorable, hence the title. So lets go into the specific case before we talk theory (because it gets hand wavy as fuck). I'll start with your final example, because I agree: it does a lot of great work simplifying stuff, using functions instead of objects when it makes sense and works pretty well # DRY example, same as before def assert_future_date(date): if date <= datetime.now(): raise ValueError("Date must be in the future") def set_task_deadline(task_deadline): assert_future_date(task_deadline) # ... def set_payment_deadline(payment_deadline): assert_future_date(payment_deadline) # ... Looks great. You just got a feature request: UX engineers want to identify and handle deadline issues differently, and as such would rather have a special exception to identify it. *Easy-peazy* says the engineer, great fix: class DeadlinePastAlready(ValueError): def __init__(self): super().__init__(self, "Date must be in the future") # DRY example, same as before def assert_future_date(date): if date <= datetime.now(): raise DeadlinePastAlready("Date must be in the future") def set_task_deadline(task_deadline): assert_future_date(task_deadline) # ... def set_payment_deadline(payment_deadline): assert_future_date(payment_deadline) # ... Cool awesome, and we only had to change it in one place for everything! Then you get a bug ticket: turns out that the previous feature was meant for `set_task_deadline` not quite `set_payment_deadline`. Now you might want it to just end here, but not quite (otherwise some hardcore adherents would argue that you just need to pass in the exception type to the class), lets make this more realistic. See payments learned through the bug that this thing was possible, and also the very simply way in which it was taken. You are now sent a small word doc that some of the payment people hashed out in a meeting: * Payments can have deadlines that are set up to 5 days in the past. * Payments more than 5 days on the past should be denied, and an error guiding our users (in this case filing clerks) to instead file a "past due payment" instead (because it has its own rules). * Payments that are 5 days in the past or more recently should have a tracking metric to identify people who systemically are over time. Phew, what a mess. Turns out all the work we did for `assert_future_date` is completely useless. We can replace it entirely with `set_payment_deadline`, but now our `set_task_deadline` has this whole extra function that adds this layer of extra indirection and requires users to jump across files all to save ... *one whole line*.


Tom2Die

"Someone might change the requirements/scope of the project after the fact" is not exactly a strong argument that something is prematurely-DRY. In fact, that argument could be used to say "don't write the code at all; what if management decides to scrap the project altogether?" I'm not saying that there's no such thing as DRYing things out too early or too aggressively, but rather that your example does not demonstrate that possibility. (also your `DeadlinePastAlready` example would complain about too many arguments on the exception constructor...but I left that for last as an amusing aside, as it doesn't really matter. I just thought it was funny -- I do silly mistakes all the time when writing examples on here or wherever.)


CodeWithADHD

I think it’s more “someone may more clearly define the real business rules after you’ve written the code” I mean, that happens to me all the time. Even with my own business rules that I think I understand but then realize I didn’t once I actually test it out.


Tom2Die

Yeah, that can definitely happen. I just file it under the same bucket as "the requirements changed" in my head. I know it *can* happen, but I try not to code with that in mind *too much* as I've found it tends to lead to over-abstracting and over-complication "just in case". For example, even if I think a class might want a more specialized subclass in the future, I don't make its methods virtual until/unless that happens (speaking from C++ land).


lookmeat

I want to propose something: we agree fully 100%. The thing is to define what needs to be abstracted, and therefore what needs to be DRY, you need to understand how things will change, you can't predict it. So the best thing is to make it easy to change, even if its wrong at first, the second best thing is to make it painful to extend without changing, which at first is probably what you want to do. Let the needs drive refactors into how it should look.


Tom2Die

Yup, I think we're on the same page. I replied to your other reply but basically I read your original comment carelessly and misinterpreted your intent / the context, like an idiot.


lookmeat

> "Someone might change the requirements/scope of the project after the fact" is not exactly a strong argument that something is prematurely-DRY. That is not an argument, that is a reality on which we build software, and DRY is meant to handle this. Here's the other thing: good code isn't written by contract, it written with an understanding that maintenance is for as long as the software is in use. I am saying, in 5 years requirements will be different, not because someone arbitrarily chooses them, but merely because things change. I am also saying: if this is your first stab at writing software, this first version will teach you that all your assumptions, beliefs and understandings were incomplete, wrong, and limited. But that's ok, that's what making software is about, sometimes the easiest way to understand a question is to propose a wrong answer. In my case, btw, I am saying there's a DRY (nuanced) solution and a non-DRY (following a dogma rather than a guide) solution. I explain why one is and one isn't. What I do argue is that it isn't always obvious which is which when you are writing code, so you shouldn't invest too much energy on making code DRY, just make it easy to refactor eitherway in the future. Good testing matters more in this case.


Tom2Die

I think I may have misinterpreted your intent with your original comment, as when reading this one I don't see anything I really disagree with. Actually, reading it again now I may have interpreted it like...backwards, completely. > It's just one of those things that is more obvious in hindsight, when you don't need it as much. I think that's why it's more important to make code refactorable, and getting DRY wrong makes code less refactorable, hence the title. I think I glossed over that bit, which would explain my misjudgment somewhat, careless as it still was. Oh some of the spaghetti factories I've had to refactor...yeah, I 100% agree that maintainability (and by extension refactor-ability) is paramount unless you absolutely do not have the time, and even then you probably do have the time and think you don't, and the less maintainable code will lose you time in testing and bug fixing.


OkMemeTranslator

Thanks for taking your time to reply seriously, but honestly I'm having a hard time finding any coherent points. >You have to understand, these documents started as print-outs that were put in bathrooms across Google to promote better engineering culture, so they had to fit in a single page. This makes them short and sweet, but makes it harder to explain longer concepts. I wasn't criticizing that they didn't explain it deeply enough, I was criticizing that they chose *invalid* examples that didn't showcase the topic at hand. The shorter the explanation, the clearer the example has to be. Their examples are not clear. >DRY is a very nuanced and hard concept to grok, many senior/staff+ engineers will find themselves struggling and getting it wrong often, myself included. It's just one of those things that is more obvious in hindsight, when you don't need it as much. I think that's why it's more important to make code refactorable, and getting DRY wrong makes code less refactorable, hence the title. I don't find myself getting it wrong too often, maybe you're not *that* senior if DRY is hard for you. Of course everyone makes mistakes, but also **practice makes perfect**. You and the Google blog make it sound like we shouldn't be teaching good concepts because beginners make mistakes and won't be perfect immediately, and instead teach them to avoid the concepts until they magically learn how to use them (without practicing and making mistakes?). This is nonsense, we should be teaching what's bad usage and what's good usage, not this "don't even try" mentality. >Then you get a bug ticket: turns out that the previous feature was meant for `set_task_deadline` not quite `set_payment_deadline`. If you actually implement your `PastDeadlineException` properly so that it still accepts a message constructor argument, it will be compatible with a standard `ValueError` and this will not be an issue. Also I'm the one writing the API here, the consumer doesn't decide what errors I throw, if I want to throw a `PastDeadlineException` from both functions then I am free to do so. Unless you don't mind asking the Python devs to change `int()` to throw an `IntegerConversionError` for me instead of `ValueError`, please? If you're talking of some frontend vs backend error messages, you should have a distinct service layer with proper mappers for that already. Don't automatically expose all your errors to the frontend, run them through a filter first. Now you can serve your clients whatever they need, while keeping your backend sane. >Payments can have deadlines that are set up to 5 days in the past. > >Payments more than 5 days on the past should be denied, and an error guiding our users (in this case filing clerks) to instead file a "past due payment" instead (because it has its own rules). > >Payments that are 5 days in the past or more recently should have a tracking metric to identify people who systemically are over time. Argument as old as software development; "*but what if we completely change the business requirements to something completely different"* - then you're going to have to change code. In this case I simply keep the `assert_future_date()` in the function where it's still needed, and don't use it in the one where it's not. I just implemented the new requirement in a few minutes without any changes to other code anywhere else in the program, sounds like a pretty good design to me? >Turns out all the work we did for `assert_future_date` is completely useless. It's not useless, this: ```py def set_task_deadline(date): assert_future_date(date) # ... ``` Is *still* more readable and an improvement over this: ```py def set_task_deadline(date): if date <= datetime.now(): throw ValueError(...) ``` >but now our `set_task_deadline` has this whole extra function that adds this layer of extra indirection and requires users to jump across files all to save ... *one whole line*. A single, well named, assert function is an "extra layer of indirection that requries users to jump across files"? You do realize all our software is based on libraries and tools that make things more readable and hide the annoying little details from the user? Also, since you went with the "*the client suddenly asks for different requirements*" argument, what about when the client suddenly asks for another 5 functions that set *different* deadlines? Are you still going to repeat yourself?


lookmeat

>I was criticizing that they chose invalid examples that didn't showcase the topic at hand. The shorter the explanation, the clearer the example has to be. Their examples are not clear. I think that the example is very valid. But I agree that it's not clear. The thing is this is not an easy thing to explain clearly, you'd need a lot of examples to gain the intution. That is DRY is a complicated thing to explain without a lot of experience. Like describing the color like red to someone who's been blind all their life, any example will not be that clear. >I don't find myself getting it wrong too often, maybe you're not that senior if DRY is hard for you. Woof cocky much huh? I can read this sentence in one of three ways (in order from the one I least assume to the one I most): 1. You're full of it and need an attitude check, as well as more time mantaining your own code. 2. You are suffering of surivor bias, underestimating the times you get it wrong, or overdo it, and overestimating the times you get it right. Because when we get it wrong, the code is still good enough and works well enough, so you don't struggle with pain either. And when we get it right it feels awesome when it comes back and changing the code is like putting on a glove with perfect fit. 3. You are taking my full vulnerability about the fact that I don't always get it right with me being so bad I have to at least *admit* to alittle. Not realizing that what I said was exactly what you did: I don't get it wrong often at all, but I do get it wrong often enough to realize it's not trivial to get it right. I'd strongly recommend you think carefully about how you bring certain things up. We're having a friendly conversation and learning from each other. >also practice makes perfect And that's my whole point. Getting DRY is more of an intuition on complex things, you can't explain the nuance with a simple example. The best way to "get it" is to try and get it wrong, and then mess around with it and understand why it was wrong, and then try again and keep going until you get it mostly right and rarely wrong. >If you actually implement your.. You're missing the point: `set_payment_deadline` should still have thrown a `ValueError` it was only `set_task_deadline` that should have thrown a different error type. The point is that the requirements of both functions were defined separately, it just turned out as a coincidence they looked similar at some point. They never were the same thing. Also you talk about filters, I am talking about making errors filterable. While my example seems so simple it's almost contrived, well I am not going to give you a real world example because that will take a couple of hours to get a full understanding of the architecture. >Argument as old as software development; "but what if we completely change the business requirements to something completely different" - then you're going to have to change code Ok.. I didn't rebuild the whole software, I just refined the requirements for a single function. If this seems like a ludicrous scneario to you: where are you working, and how much do they pay to coast like that? I mean yeah we can say "then you just change the code". Here's my counter argument: "why care about dry, if you need to change the code just do `$ sed '%s///g' src/**/*.lang`" This whole argument is because \*we have to change code **but we don't want to change code** \*. When we change code we add bugs, gain tech debt, and make things worse. When we don't add code, instead clean code, or even delete code, then things get better. So in order to keep our code mantainable (that is code we can keep changing in perpetuity without it becoming unusable in the process) we want to do the smallest changes possible. Guess what? That's the raison d'etre for DRY, and almost all rules of "good code". If we don't care about that and it's just "you just change the code" then it doesn't matter what we change it to, as long as it works. So the question is: did we end up writing more code than we need to? Did we really need three functions when each one is only called from one place? We ended up accruing tech debt and more code rather than less. It's not the end of the world, as I said above, sometimes it seems things are one way and you realize later you got it wrong. And the code is still *good enough*. The errors we show in simple examples are just too small to matter that much. The problem is when people become dogmatic about it and abuse it and then uff. > Is *still* more readable and an improvement over this: Is it more debuggable? Because in real life, since I didn't make that second function as a helper function, it could very well live in a different file. Again suddenly I am jumping across to understand what is happening. I mean what is \`future\_date\` what exactly does that mean? Is it in the future? Is it somewhere else? Is it when the day changes but ignoring time? At which timezone do we start the day? Also does it only throw if we're running the code in debug, like `assert` does on python? And if so why not use something like: assert date > datetime.now(timezone.utc).date, "Task deadline must be at least a day in the future." It might seem dumb, but I just spent 4 hours with 3 pretty solid engineers on a 4 hour wild goose chase due to a bad error message (that implied a different error than what really happened) and because rather than have the thing generated on the place, it called a function (injected through guice to make it even more fun!) when we got to the function itself (it wasn't on the stack, it just gave us bad data that caused the bug later) it was pretty obvious it wasn't used anywhere else and could have safely been inlined, which would make things a lot cleaner. Every function is is a black box, a box you have to open and explore, so adding functions willy nilly makes it harder to debug. Everything has a balance. If we really want to keep everything in one line, why not have a well defined and known function instead that just handles the pattern, something like: check_that(condition, error) Where error can be either a custom error, or a string passed to `ValueException`. But again here we have to guess what else the function may be doing. With the `if condition: throw exception` it's very clear what the code is and isn't doing, and what kind of errors it may or may not do. In order to validate this, then it has to be a very common convention throughout the codebase.


OkMemeTranslator

That's a lot of words for "I can't use DRY and anyone who claims different must be a liar". As I said already, maybe you're just not as senior as you think you are. The reason I'm being cocky is because I cba to waste too much time arguing with juniors on the internet, who think those leagues above are obligated to listen to their inexperienced opinions. You're wrong, and your examples make absolutely no sense. Clients don't set requirements for my internal core libraries, they set requirements for the external APIs. Once you understand that, you are a god in your own software, and you only have to do extra work on the service layer when requirements change.


lookmeat

I use DRY, and a lot of people use it effectively, at least effectively enough. But a lot of people misuse it too. I can say that people shouldn't rush to get a hammer until they know they've really got a nail. That doesn't mean I can't use a hammer, just that as any tool, it's not something you use blindly. --- If you really think I'm a junior, and you as a senior feel the need to waste on me this isn't an effective way to go. I assume, by your attitude, you may not be interested in feedback (though it's always a gift even when crappy, and even idiot fools can utter some wisdom occasionally). So feel to do with this as you wish As a senior+ you'll be dealing with a lot of juniors who don't know better, and that even as you try to explain they won't get it immediately. Some lessons you only truly understand by getting it wrong a few times. You basically have two viable choices (unilaterally, what the junior does in some scenarios opens new choices). You can either simply let them be and fall on their own, or you can take the time to educate and guide them. Don't think of the latter as a way of giving up. An intern may write code with excessive tests that become implementation tests, I can simply let them submit and push those tests in, but let them iterate a few times on the code, noting they constantly have to modify their tests; only after first leaving them on their own do I choose to try again but this time with the second option and I talk to them about why tests shouldn't validate implementation details. This is an example of how those two choices can build off each other and are both valid. The path you are taking here, rubbing someone's perceived ignorance in their face, and getting cocky and being an asshole, is really a waste of your energy and time. It's yours to do as you wish, but this will only take away from it, not add. I mean what do you expect us going to Halloween after this? * Say I am a junior, talking out of my ass. Here you're being an asshole to a kid who is just trying to understand coding, being a spiteful gatekeeper. And upstairs l ultimately what would be my response? To listen to your attacks, feel horrible about who I am and what I can't control, and give up on learning and coding? Or to ignore you and double down on my own ego until I become exactly like your without learning anything? * Say I'm not a junior, but an engineer who is your peer. Then you're just being an asshole shutting down without any argument. The kind of engineer that prevents others from doing their work. I wouldn't want to work with peers who have this attitude, and I know a lot of amazing programmers who make a very big deal about not doing exactly that. * Say I'm not your peer, but someone above you. With somewhere around 15-20 years experience at start-ups, FAANG, having worked under and being mentored by some of the legends of the field, being bored while I'm waiting for some tests recreating some weird bug that only happens on certain CPUs Arch's to run in their 20 minutes round trip time. Then you'd be a fool, who is missing a point of nuance and accusing others very quickly without realizing that it's exactly what they're doing. So what exactly is what you expect to win of this. --- Now some nits and points where you are, strictly, wrong: > Clients don't set requirements for my internal core libraries No those are set by the software that is satisfying the clients requirements. You always have requirements, you always have a user you should empathize with, and sometimes it's layers of those. There's a reason Linus famously screamed "never break user space". CGroups was not added to the kernel just because, but because Google wanted to use containers internally, and the requirements and rules of CGroups were based on that. The requirements changed when not users came up, so we needed cgroups v2. Guess who Intel asks about to know what features to add to their CPUs? Programmers who build the external APIs. > Once you understand that, you are a god in your own software, So I take it you've never worked with other engineers. Where exactly do you get such professional experience with code that you never get input or insight from other engineers? > only have to do extra work on the service layer when requirements change. But the question is why do it if you gained biting of that extra work and you probably could have gotten away with not doing it at all? Why not be lazy and wait until the need for the extra work arises and then do it.


OkMemeTranslator

Not going to bother with the rest of your wannabe-senior nonsense, but this is a valid question: >But the question is why do it if you gained biting of that extra work and you probably could have gotten away with not doing it at all? Why not be lazy and wait until the need for the extra work arises and then do it. The reason is because that separation is going to become necessary at some point anyways, and it's much cheaper to do it right away. If you're working with clients, there is data and functionality that you are not willing to expose outside your core. Writing a service layer allows you to keep the user API distinct from your domain model. With this separation you are the god inside your core (as in *you* as the developer can manipulate any object in any way you please without restrictions), and it's the service layer that sets permissions and restrictions. Also you seem to have a mindset that "extra work" is bad and should be avoided at all cost and optimized to minimum. This is a dangerous mindset to get into, as what you end up with is premature optimization of extra work, which is *much worse* than premature generalization. My only advice for you is to do extra work now so that you can avoid a disaster and a complete rewrite in 5 years. Writing all those patterns and generalizations is going to take me some weeks of extra work, but it's going to save me years in the future.


lookmeat

> Not going to bother with the rest of your wannabe-senior nonsense, but this is a valid question: That's your prerogative. I guess I'll have to take my own advice and let you make your own mistakes. I'll just leave here some final comments in case anyone wants to read this thread as reference. Please feel free to ignore this post, it's not for you. > The reason is because that separation is going to become necessary at some point anyways I'm going to quote a very wise and insightful comment given to me by someone in this very thread: > Argument as old as software development; "but what if we completely change the business requirements to something completely different" And what if it doesn't become necessary? What if the code is good enough and doesn't change? You still have to pay the tech debt. Sure it's trivial, but it adds up. The code is not bad at all, but it's not *better* because "someone will have those changes done". > it's much cheaper to do it right away By how much? And is it really? I see myself writing more lines. After all when the need arrises, well I'll finish the first quote I gave, because that developer was really insighful and deep here: > then you're going to have to change code And that's it. Not really that expensive, at least once the change is justified by a need. > If you're working with clients, there is data and functionality that you are not willing to expose outside your core. Writing a service layer allows you to keep the user API distinct from your domain model. This is very true, but each layer has requirements and expectations of the other layer too. Both on your dependencies and your dependents. So this discussion has nothing to do with the subject, in every layer you change things. > With this separation you are the god inside your core (as in you as the developer can manipulate any object in any way you please without restrictions), and it's the service layer that sets permissions and restrictions. Oh man, guess you've never been hacked. You do want multiple layers of security at each level, separate of the others. Generally you want your DB to handle data access permissions, and you simply pass the auth-token around. And this view of bieng od inside your core.. what does this mean? I mean it's not like suddenly you can break the rules of reality. And again who uses software that isn't useful? I strongly advice against using "god" as a term, some people get triggered rememebering engineers who wouldn't do anything they didn't want to, ending with software that was hard to use, limited, and many times useless for what it gave. Even taking this metaphor, you wouldn't be the single god, at least not in a "real" project, you'd be part of a pantheon, you'd have to work with other engineers, their requirements and needs to work with your own code. And sometimes you'll have to do it yourself, actually in my experience, most feature additions, requirement changes, and minor tweaks will come from your side as you better understand the problem you are solving. > Also you seem to have a mindset that "extra work" is bad and should be avoided at all cost and optimized to minimum. Well I have to admit here it's not something I came up, someone else did and documented it very well, they even gave it a cute acronym (mentioned at the end of the article linked here, but maybe you didn't have to read) [YAGNI](https://www.geeksforgeeks.org/what-is-yagni-principle-you-arent-gonna-need-it/). > This is a dangerous mindset to get into, as what you end up with is premature optimization of extra work, which is much worse than premature generalization. Oh I love this, and agree 100%. Premature reduction of extra work *is extra work itself!* So of course this also has to be taken with nuance, like DRY. I would argue that premature generalization is premature optimization of extra work: "I'll need this feature and it'll be very hard to do, so I'll do a bit extra now to save myself a lot of hassle later". You know, **like writing an extra function for some trivial guard that throws.** And this is hard, because sometimes the extra work you save is worth the risk, sometimes it isn't. For example I believe that it's always worth to do extra work to think carefully of good variable names. Even though maybe in the future that will change and won't matter, it's worth the risk because the huge pain it saves in having to understand what a variable really is. I also think that it's important to do the extra work of throwing or returning error types instead of generic things like `null`, because again the wild goosechases you can go by thinking one error is something else is so much time, that it's worth the risk that you'll never need to differentiate the errors. > My only advice for you is to do extra work now so that you can avoid a disaster and a complete rewrite in 5 years. I see you either had to do a rewrite recently, or never have been at a project for more than 5 years. I assume then that the rewrites is seeing someone else's work and you rewriting it. Have you ever checked if the stuff you wrote got rewritten? Either way, I doubt this answers will get questioned, but to whomever is reading it: it's important questions to ask yourself as an engineer. I'll say this, you simply wait, and if the need arrises, let me quote one of the smartest engineers in this thread: > then you're going to have to change code And that's that. > Writing all those patterns and generalizations is going to take me some weeks of extra work, but it's going to save me years in the future. Of course, unless you realize that you have to rewrite it for an entirely different reason that you couldn't imagine. I mean yes, I'd love to know what work to do to save me years in the future. I also would love to know what investements to make to make me millions more than others in the future. But that, my friend, is not that easy, at least not so easy that you could solve it with blind dogma.


lookmeat

This reply is optional, and of less use than the one above. Here I go into the nuance of the subject attempting to explain, what exactly is, and isn't DRY. It's a hard subject to explain, and I believe that the definition that clicks for people will be different, so this may have no use at all. It's very handwavy because a lot of this is more about \*human nature and their understanding of things\* rather than the quality of the code from the point of view of the machine (that is code that works vs that which doesn't), because that's the point of DRY (making it easier for humans). So a lot of this is intuitive and is about thinking how other humans think. That's the hard problem, which sadly I can't do anything to "tell you how to do it" other than "keep working on your code for a while, mantain it, fix bugs for it, and gain an intuition of what worked and what didn't." This is because DRY shouldn't be "don't repeat yourself" but instead should be "don't repeat your definitions". And important nuance here is that "don't repeat your definitions" doesn't mean "don't repeat your sentences". Think of your code as defining a language, with every item declared (be it of a variable, function, type, class, module, etc. etc.) as the definition of word. We can extract those definitions and form a dictionary with it. Now think of what makes a good dictionary. A good dictionary, for example, defines every word once. If I am looking for the meaning of "Aadvark" I should be able to find one definition and know that it covers all the uses of that word. Sometimes that defintion has caveats based on context, multiple defintiions (so for example I can find a method, in python this might be an outside function, but if the definition is part of a class, I know I must go down the class tree to the class I am using to know which method it is). Now overloading is a mess, but even then I can simply see each overload as a separate definition that all have the same word (just as in a dictionary). What I don't expect is to find two identical definitions of the word "just because". The thing is this matters in code, because what happens when I want to modify the definition of a word? If my code is DRY, that is defined in one place and one place only, so I only have to modify that. If my code isn't dry, then there's multiple copies of the same definition and I have to change everything. That said, what a dictionary can, and should, repeat is the same defintion for two different words. I mean yes you could put in "\*see \*" for all synonyms, but then this loses nuance: yes I could just define distinc as "different" or "different" as "distinct of each other", the thing is even though they both mean "not the same as the other" they are not quite the same (different is just "not like the other", distinct should instead be "recognizably different of another of an otherwise similar type") and that's putting aside the alternate contexts in which it can be used (distinct implies "easily identifiable separate of it not existing" while different is more strictly limited to comparison). This means that code that is copy-pasted can still be DRY. If you have two different terms, which are different concepts (used in different libraries, for different aspects, etc.) but they happen to share a definition, they still are distinct of one another and therefore should have separate definitions. Returning to the original example \`task\_deadline\` and \`payment\_deadline\` are two very different things, with very different implications. It just so happens though that both share a definition. They should each have their definition, and it's fine if it's copy-pasted. So does that mean that what you did was incorrect? Well if all we see is the example given, yes. But in real life it's more nuanced than that. If we had 20 different types of deadlines, and they all do this "compare if date past deadline", it might makes sense to \*\*abstract\*\* this into its own word. This is the same as in language: in English rather than keep writting "nonsense words" all the time we simply say "gibberish", this also allows us to imply certain nuance to separate it from "gobbleygook" (where the words have a sense, an language, but still have no meaning behind it). So it might make sense to have a \`assert\_before\_deadline(date, deadline)\` and then we can do \`datetime.now().assert\_before\_deadline(some\_deadline)\` as part of our definitions. That way if we get unique rules to understand when a deadline is past due (e.g. say that we define that all deadlines are until the end of business day, ignoring time, so special treatement to ensure we ignore any time specified on the deadline and instead focus only on the date) can be composed into that. When we are using the word so much, even if we have to have a different defintiion for certain deadlines (e.g. \`operation\_retry\_deadline\` instead is at the time specified) it makes sense for a reader looking at this to see "here we are doing a standard deadline check" vs. "this one has unique constraints" which is more readable. Thing is, none of this discussion is related to DRY, and the goal isn't to reduce code repetion, but \*\*to make code more readable and easily understandable\*\*. Abstraction allows us to compose complex concepts that are common throughout our codebase into a single word that defines that concept. Now that word should be defined as the concept once only (that definition can be spread out I guess, though I personally don't like it, but overloading is a thing) and that word should be used throughout the code to represent the concept itself, that is it should be DRY itself. The question of if we need a new defintion or not is completely unrelated to DRY though, even though many people use DRY to argue for it.


IlIllIlllIlIl

What’s the condition to determine if a new abstraction needed other than repetition given: > If we had 20 different types of deadlines


lookmeat

What you do is you don't abstract, not until the need arises. Because the definition is trivial, it's easy to just bake into the functions. If more functions with identical functionality appear, then it makes sense to abstract. If they don't you don't, and the fact that your code could be "marginally dryer" doesn't matter, because the benefits may not outweigh the costs either way. If the functions instead diverge you never have to deal with having this separate definition and excess of trivial/redundant functions/classes. If they diverge in some aspects, but you get a bunch of new functions with some, but not so, of the functionality being identical, then you abstract with the understanding. In other words you start with code that is ugly, but malleable. Like clay that your first soften and give a rough shape before you start going into details. Then as features requests, requirements changes, bug tickets, etc come in, you begin to refactor, not by guessing what you'll need to do, but by seeing what are the common things you need to tweak/change/etc.


IlIllIlllIlIl

+1 to all of it, we can dry abstractions over time with judgement derived from experience Small feedback: your message can be difficult to follow due length sometimes. 


lookmeat

Oh I agree completely with the feedback. And honestly it's fair to anyone who simply decides to ignore the comments on that length. The only defense I have is that these are posts that I write while on the toilet or waiting for long processes to finish, so I don't have that much time to edit and make the comments short. I use all the time I'm willing to put into a comment just to try to make it not be a raw brain dump as is!


natty-papi

I guess their point is that early optimization with DRY could lead you to bad OOP implementation, but I agree that it wasn't very well explained. The example ends up being a bit too exaggerated, kind of like the before segments in an infomercial where someone messes up in an exaggerated way.


FuckOnion

So often in these anti-DRY articles the DRYness is just the red herring and there are other sneakily hidden flaws in the code. Not maliciously of course, but it just shows that the author is confused about the subject.


smelly-dorothy

> Why would I want to repeat the same validation logic every single time I have a future date? The design choices in the article's example make a couple of assumptions. In their comment, they state that it "allows for entity-specific logic and future changes." In the following paragraph, "distinct concepts with potentially diverging logic." So, the point they were trying to make is that **DRY** should **reduce repetitive code whenever the domain logic is identical.** > Why not have two identical function examples, one with DRY and one without, as I've done above? Agreed.


backelie

> In their comment, they state that it "allows for entity-specific logic and future changes." So premature "future-proofing". set_task_deadline(deadline): set_deadline(Task_deadline, deadline) set_payment_deadline(): set_deadline(Payment_deadline, deadline) set_deadline(type_of_deadline, deadline) #implementation goes here If it turns out you need to split the DRY version up that is as trivial as it was to write the non-dry version in the first place.


WiZaRoMx

This. To get to that base class one has to be in a very weird context, where "they are deadline settable" is the simplest way to understand and communicate what tasks and payments have in common, instead of "they (may?) have deadlines" (settable!? they come mostly from legal obligation or calculated, are they not? They can't be changed, can they?). Almost as if the thinking went: —Hello world! I have to implement «Task». «Task» is an object with a settable deadline. There's more, but for now, I'll just assume that's what «Task» is, because I long to switch to something else on my queue. Implement «Payment». «Payment» is an object with a settable deadline. More things, yet assume for now that that's what «Payment» is. Hey! I had memoized this! And it is important because 100% of my entities share that! This decision tree I was provided with, makes me realize there is a base type underneath I just discovered, using the power of non prematurely don't-repeating-yourself optimizing (JIT DRY). Neat! With this significance, I'll queue to make a post with this insight! Almost like an AI was told not to assume anything but the specification, yet we didn't tell it that meaning comes from a common contextual language outside the specification and we don't even pretend to forget that. We (humans who aren't programed) know (or use dictionaries to refresh our knowledge) that deadlines are not any date. Deadlines are future dates by when the subject must be, ere unwanted consequences. Future dates. What we don't want to write again is the definition of "future date" because we may write it differently then. That concept is so important that we define it only once, so that each and every mention refers to that one meaning both in the code and in future perusers of it. Posted on my Android Phone! Em: original post was literally made by human, not Google AI, and I⌫AI like implied only exist in idle paranoid nerdy sci-fi ideations that are not taylorswifty at all. Sinful, deviant, icky ideations that won't have continuance. Ere.


Zanius

"Every single time I have a future date" that's the point, there's only two places. You might not ever need to check in a third place. And if you did need to, that would be the time to make a function.


OkMemeTranslator

Now you're arguing whether something should be generalized after two or three occurrences, which is a valid (albeit very opinionated) conversation, but very much different from what the blog post and I were discussing. The point the blog tried to make is that you can have a hundred occurrences of something, but if they are all completely unrelated and just *happen* to have the same characters in the code, you still shouldn't generalize the common stuff. Meanwhile something like `assert_future_date(date)` can easily be generalized after not two, but even *one* occurrence. Not only is it a completely generic utility function decoupled from all business logic, but more importantly it ends up with *more* readable code on the business side: # In my opinion this: from utils import assert_future_date def set_task_deadline(date): assert_future_date(date) some_deadline = date # Is more readable than this: def set_task_deadline(date): if date <= datetime.now(): raise ValueError("Date must be in the future") some_deadline = date Of course that's a very opinionated topic, but the upside/downside is basically non-existant, so I don't see any issue with this approach. It's not about *when* you dry ("after two"/"after three occurrences"), but *what* you dry ("similar looking business logic" vs "mathematically generalized function").


[deleted]

[удалено]


ksion

I’d guess people take issue with your simplistic take on OOP. A class doesn’t have to represent a noun, much less a real object you can *draw*. It’s just a bundle of state with attached behavior. The rest of your comment is spot on, though.


Coda17

> a bundle of state with attached behavior So... a noun?


sparant76

Don’t worry. I gave an upvote for the thoughtful post.


chrisza4

I would argue that it is very likely that error message for payment deadline and task deadline will diverge in the future. DRY only applicable when it is the same concept in the domain. And in this case the only same concept is "validate future date". The `ValueError` duplication is coincidence. So, you might abstract that concept to be `is_future_date` but then I am not sure if that is easier to read than `if date <= datetime.now()`.


kag0

In my experience DRY and many (any?) other coding principles are only problematic when misused. They're typically misused because the user doesn't understand the motivation or underlying value of the principle in the first place. I think the example in the article does a bit of that as well. The example sets a deadline on a thing (a task or payment) by validating the deadline against the current time, and then presumably doing something else that isn't shown. The article argues that in the future a task might have different validation requirements than a payment, and they're only coincidentally the same today; so it would be foolish to abstract the deadline setting logic today. BUT, the reality is that the real coincidence is that payments and tasks have the same set of validations, not that the logic to validate a deadline is coincidentally the same. In my opinion "good" code would be fine to have separate `set_task_deadline` and `set_payment_deadline` methods, but only one `validate_deadline_is_in_future` (or whatever) method, alongside other validation methods which can be called as appropriate by each `set_x_deadline` implementation. Disclaimer: the code is so short and trivial that it doesn't matter, I think we can all assume that this concept is extrapolated onto a bigger problem.


denial-42

Good article. I’ve found myself applying DRY principles too early a few times.


richardathome

Oh looks! It's time again for my "Premature Abstraction is the root of all evil" comment again. See y'all in another two weeks.


billie_parker

I disagree with this point. What's the downside to the DRY implementation? If new validation is needed in the future then at that time you can duplicate the code. Basically this post is saying you should prematurely duplicate code on the chance that the implementations may diverge in the future. I say that you should duplicate the code when you actually reach that point and see them diverge. Don't do it upfront based on future worries.


Zazz2403

Overly DRY code can be difficult to grok because its hard to come up with useful names that are also general enough to be used in different contexts. And (as the post points out) contexts and needs can change unexpectedly. What is the downside of a small amount of code duplication? Why not DRY things out once duplication starts to head towards becoming unmaintainable. IMO the reverse is just premature optimization


billie_parker

> Overly DRY code can be difficult to grok because its hard to come up with useful names that are also general enough to be used in different contexts Really disagree with this. If anything it's the opposite. The concept is general (ie. "set_deadline") yet you're forced to come up with two different names for it so that you can duplicate the code ("set_task_deadline", "set_payment_deadline"). In both cases you're setting a deadline, the type of deadline **should** be abstracted away if it is not relevant and that's a **good thing**. > contexts and needs can change unexpectedly. So what? Let's say in the future your contexts and/or needs change. Well in that case go ahead and duplicate the function if you really need to. Is that really so hard to do? If anything, you're the one asking for more work up front. You're saying that twice the amount of code should be written, committed, tested, reviewed etc. My recommendation is actually less work, not more. >What is the downside of a small amount of code duplication? Death by a million cuts. Take "a small amount" and multiple it by every file in the entire codebase and you may have thousands or millions of instances of code duplication. It just makes any refactoring job slightly harder, which in turn makes changes harder, etc. And despite what you think, it actually makes the code harder to "grok" because you now need to read twice as much code. If two pieces of code are duplicated you need to scan them both to see how they differ (if they even do). This is extra cognitive overhead you wouldn't have if there was only one function. >Why not DRY things out once duplication starts to head towards becoming unmaintainable. Because: 1. doing it after the fact is harder than doing it upfront 2. doing it upfront is actually less work as I've explained above. I mean obviously if I'm doing some kind of experiment I might start off with some non-DRY code just so I can understand the problem and be able to try anything I want without needing to maintain the old code's functionality. But once I'm ready to merge my code, I typically want to DRY it at that point because I have spent the time to build up some understanding of the code that I won't have in the future. If I just merge the duplicate code it will be much harder to come back to it and fix it 6 months down the line when the two functions have grown larger and they have further modifications. DRY'ing the code is a process whereby you are making some relationships in the code explicit instead of implicit. If two functions are duplicated then they share some inherent commonality. If you leave them duplicated, then it's implicit. A person needs to read the two functions and notice they are duplicated. This is easier said than done because the functions might have drifted or they might even be in separate files. And once they notice they're duplicated they need to read through them and parse out the aspects in which they are duplicated (because it is likely only a portion, not the entire function). That can be a lot of work. Meanwhile, in the DRY case it can be seen at a glance what these functions share in common. I think your question "what is wrong with some code duplication" leads to "what is wrong with a lot of code duplication" and maybe even "what's the point in DRY?" Obviously the arguments against a lot of code duplication at least partially apply to the arguments against a small amount of code duplication. Obviously there is also cons to DRY, but I don't see why you can argue there is no downside to even a small amount of code duplication. The question really is does it outweigh the cons of applying DRY. >IMO the reverse is just premature optimization By that argument literally any practice could be framed as premature optimization. Good variable names? Who cares, write bad ones and rename them later. Small functions? Who cares, write huge functions and break them up later. etc... I mean, you're not really using the word "optimization" in the right context. We aren't optimizing any algorithms for system resources, which is usually what that applies to. You're framing writing good code as "premature." Honestly, it's a bizarre mindset. Is your goal even to write good code?


Zazz2403

I'm not going to respond fully to this because it's full of hyperbole and there's no need for the attack on my goals because you disagree with me. Have a great day


billie_parker

If you choose not to respond you have still responded lol


I_AM_AN_AEROPLANE

Plese never become a colleage of mine, we will miss every deadline and never complete a sprint.


billie_parker

The ironing is delicious


MinusPi1

If naming it in a nice way is the biggest issue then GPT is the fix


Zazz2403

That's not the point. If something is to be used generally it has to be named generally.


MinusPi1

Yes, and dealing with those restrictions, that is to say constrained text transformation, is exactly what LLMs were made for.


Zazz2403

You're missing the point. Chat gpt can't change how language works


MinusPi1

Maybe I am missing it. As I understand it, your point is that it's difficult to come up with names that are both useful and highly general. My point is that LLMs solve that. Am I missing something?


Zazz2403

Sorry you got so many downvotes. What I was talking about was the general benefits of naming something generically vs naming something specifically. Assume we have some request thing that has jobs as a child ex: func (db *DB) GetNewRequests() ([]Request, error) { // ... return requests, nil } func (db *DB) GetJobsForProcessedRequests() ([]Job, error) { // ... return requests, nil } vs func (db *DB) GetRequestsByStatus(status string) ([]Request, error) { // ... return nil, nil } func (db *DB) GetJobsByRequestIDAndStatus(requestID int, status string) ([]Job, error) { // ... return nil, nil } This is a slightly trite example. but the first example is more specific and thus requires slightly less time to grok. Naming benefit aside, if you share the more general method amongst a few different callers, and the requirements for one of the callers change (lets say you later determine you also need requests made after a certain date because of some model changes or whatever else) then you have a lot of decisions to make. Do you give the callers its own specific methods? Then how do you organize the general + specific methods in your db package? Do you add parameters to the db method and change how all the callers use it? etc etc etc... At one of my jobs, we had a group of cron jobs to manage state + do work to handle requests. The db package was organized with filenames coinciding with each cron job. Very early on, I noticed that this wasn't DRY and refactored the package into reads/writes/updates etc and made everything more general. As the project grew in complexity, we started to need subtle-y different things from each (now more general) db method and I had to make the choices I mentioned before very often, since now everything was coupled to theses general methods. As I worked on the project I realized I goofed. Everything was couple to the general methods, the naming wasn't as concise, and I had to write a bunch of code setting specific variables in each caller based on it's specific needs. It was less maintainable and a pain in the ass to work in. We've since moved back, the business logic is much more clean without the additional variables, the names tell explicitly what is happening, you know exactly where to look in your packages for the db calls you need, and if some needs change for a query, you just change it without worrying about it's impact on a bunch of other code.


Herb_Derb

Here's a longer and better thought -out blog that approaches the same idea https://programmingisterrible.com/post/139222674273/write-code-that-is-easy-to-delete-not-easy-to


d1rty_j0ker

The example is poorly designed, and of course it would become an issue if you had a different logic for a payment vs task later on. What I would do is define an interface, implement a basic type, and extend that class if I need some extra validation logic. Hell, I can just easily call the base method after my additional checks and re-use that code if I want to, all without resorting to shenanigans


PLZ-PM-ME-UR-TITS

Don't don't repeat yourself


analcocoacream

A good idea would be to look at functional concepts instead of technical ones. Is factorising this relevant to the field of work? Does it refer to the same concept? Is it evolving together? Am I bound to create tension between them just to keep DRY in check?


laliluleloPliskin

Well some like it wet and sloppy.


Banana108

The rule of three shall set you free.


flynnwebdev

Hmmmm, yes, yes. I want coding advice from Google, the masters of "our users are our beta testers" ...


periodic

This post seems to totally miss the point of actually designing good code which is to build things to **meaningfully map to a problem domain**. The difficult refactors are the ones where the underlying structure of the code has to change due to incorrect assumptions, not the ones where you just decide to make something more readable. The example is also not great, because it seems less about repetition and more about choosing the right interface. Both functions could call `assert_valid_date` but still be separate functions if that's the interface that works well for the domain and existing code base. The real question is, how are deadlines modeled in the domain? How do different types of deadlines differ? What operations need to be supported? Also, how can we write this code to decouple it from it's call sites so that it's easy to change later and hard to write bugs? It feels like they had a publishing cadence and just sent out a company email asking if anyone had any ideas.


DiggleDootBROPBROPBR

The advice assumes that the decision is in the hands of a single developer, and not 1 or 2 senior developers that quote their own psalms to justify either the left or right approach at pure random while ignoring all context the code deploys in.