Hang your code out to DRY
johan.hal.se
johan.hal.se
My second heuristic is: Can I name the method I wish to de-duplicate in a way that is honest for all cases I wish to cover, yet explains its business purpose?
The more it deviates from these heuristics, the more likely I am to duplicate the code in object oriented programming.
I phrase it a little differently though: "if something happened in the future that required a change to function A, would the same requirement apply to function B as well?"
I think your second heuristic is valid but a little dangerous, because being good at naming functions is somewhat orthogonal to being good at maintaining code.
However, in this case, I use it is an extension of the question "is this the same concept?" if it looks like useFloorWaxOrDessertTopping, it's a good clue that you may have the same lines of code but they are certainly not the same concept.
While it doesn't spell out a word, I think it's much better advice than DRY, and better aligns with your heuristics.
> Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
Further on:
> Dry is about the duplication of knowledge, of intent. It's about expressing the same thing in two different places, possibly in two totally different ways. [emphasis in original]
And the next paragraph:
> Here's the acid test: when some single facet of the code has to change, do you find yourself making that change in multiple places, and in multiple different formats? Do you have to change code and documentation, or a database schema and a structure that holds it, or...? If so, your code isn't DRY.
But that's the problem right there. Why is there more than one place to fix it?
(Now, sure, if you're using a function call wrong, you need to fix it everywhere you call that function, and it's fine that there are multiple such places. That's what functions are for. But if it's something like how a value is calculated... why is there more than one place that calculates the value?)
I do agree that a piece of knowledge may not be a business concept.
First off, it excuses some really egregious behaviour like copying entire large chunks of code representing an identical concept just because "I haven't copied it twice yet". I've seen an entire controller and all related views copied wholesale because it needed to be placed in another area of the app (with identical visual and functional requirements)
Secondly, if the code is only incidentally identical, even 3 times isn't enough to say that you should apply DRY. 3 independent concepts that happen to currently share implementation still shouldn't be deduplicated. I think a great example of this is the inherited resources gem in rails that implements a "standard" interpretation of controller actions - which works well at first until your controllers need to do something other than basic CRUD. The fact that say, an update action handles parameters and saving in the same way as some other update action may currently be true, but is not at all guaranteed to be true in the future, even if you have 100 update actions in your app.
But we don't always know what application we are writing.
Same. “That’s just DRY with extra steps!”
If fact X changes, how many places in the code do we have to change it?
If the number is > 1, relying on humans to realize that will, on average, always fail. I know of two ways to fix that.
[1]: DRY it up.
[2]: Write a test asserting that all the places use the same value.
Sadly, most people rely on this:
[3]: We "just have to remember" to do these changes in all places, despite the fact that human memory is unreliable, and the future person working on this code isn't even in the room when we decided this.
Any time I hear a "we just have to remember" variation, either from coworkers or in my thoughts, an alarm goes off.
[3b] is adding comments, which is better than relying purely on memory but can often be missed. I've locally improved on this a bit by including cross-references on the relevant lines, along with tooling such that the referents are automatically surfaced during code review.
i have coined the term "Locality of Behavior" (LoB) as a competing design principle to DRY (as well as Separation of Concerns, SoC) that advocates more inlining of (potentially repetitive) logic in the interest of code maintenance and understandability:
https://htmx.org/essays/locality-of-behaviour/
pic related:
https://pbs.twimg.com/media/FAInxTuVkAATRrn?format=jpg&name=...
when I was younger it was really important to me that the code work the entire time I was writing it
as I got older it got less important and now I make the code "look right" (that is, read well) and then I get it working
Exactly, so you don't have to think so much about if it will work, you can take that for granted and think more about subtler things.
That's because the original article is so clearly about tearing down bad abstractions, but a large majority of programmers - based upon discussion about the article - seem to never get past the first part of it.
Given a long enough time horizon, all abstractions turn bad. The solution isn't to not abstract. The solution is to tear them down when they go bad. And if you don't learn to tear down bad abstractions, your codebase will devolve into shit regardless of what you do.
I disagree with this analysis. Abstractions certainly go bad, but I don't think it's correct to say all abstractions go bad.
The solution I took from Sandi's post was two-fold.
* Don't prematurely abstract, it's better to live with a little duplication vs aggressively eliminating it.
* Don't hold abstractions sacred, when you start seeing an abstraction with too many conditionals, consider breaking apart the use-cases to see if there are actually 2 distinct abstractions happening.
Reading the "original SOLID paper" [1] was enlightning to me. The initial assumption is that software rots, or gets less flexible with time. The best way to prevent that is to identify the part that is the least flexible/most rotten and replace it. But for that you need two things: being able to clearly identify parts of the software, and being able to replace them. This is where modularity and abstractions comes in. But this is also where the good old delete key comes in. This is where modularity and abstractions comes in.
Building software from parts, expecting to replace them does leads to abstractions, but different ones from building software expecting it to never be replaced. And, in my opinion, the first kind is easier to deal with.
[1]: https://web.archive.org/web/20150906155800/http://www.object...
That's one of the absolute best ways to DRY. factoring out common base classes is a classic OO exercise. It's possible to drastically reduce the size of a codebase, and the potential error exposure, by doing some simple extractions.
The biggest argument that I hear against OO, is that "someone may misuse or misunderstand it."
I feel that this reflects the tech industry's obsession with hiring armies of relatively inexperienced developers, and then cycling through them, because we don't do what it takes to retain people.
I like composition. I use it often. It is not a "one size fits all" solution to anything; just like OO isn't.
"Reduce state" is another big rallying cry. Good advice, for algorithms, multithreaded service providers, and engines. Not so good, for UI, and, in many cases, device control.
I have spent the last couple of days, working on the login screen for the app I'm developing. It's loaded with state. That can't be avoided, and negotiating the several different states that this -seemingly- innocuous screen can have, is not for the faint of heart, but it needs to be done right, because it's the first screen our users see. It also optionally implements Sign In With Apple, which brings its own baggage. The users' experience must be absolutely frictionless, while also being very secure. The work has involved the server (PHP), the SDK (Swift), and the app, itself (also Swift). I'm not done. I keep uncovering corner cases.
I'm just not a fan of "Don't use X, because X is bad, and you're a bad programmer, if you use X." The tech industry has been dealing with this, since the GOTO wars.
Most of my projects are a hideous chimera of decades-old techniques, mixed with cutting edge stuff.
If someone wants to work on it, then they need to have their stuff together. I'm not going to "dumb it down," but I need to do a lot of documentation (I write about how I document, here: https://littlegreenviper.com/miscellany/leaving-a-legacy/).
Here's an interesting thing that happened to me, some time ago, and I decided to write about it: https://littlegreenviper.com/miscellany/swiftwater/the-curio...
In my experience, poorly composed code is simply easier to understand than code which poorly applies inheritance.
For me, inheritance is best used lightly. The obvious smell is when you end up with methods that don't apply to all the base classes. Or, said another way, when a super class it has a superset of capabilities for a base class.
A good example of this is Java's collections, which, for the most part, are quite good with their inheritance. However, because the base classes have mutable methods, it makes it a pain to deal with unmodifiable collections. Java's mistake is they should have had the default collection be unmodifiable and had sub classes which added mutation capabilities. List and MutableList, for example.
That is true (and it won't change, so we need to behave like it won't...), but that is not the only reason.
An inheritance hierarchy in a large program that makes sense today might not make sense in a year or two. Refactoring it is hard. Refactoring a composition-based solution is easier.
Composition-based solutions are also easier to test: inject mocks. It's a lot easier to mock a compositional dependency than it is to mock behavior that's inherited from a parent class.
Given that composition is easier to maintain and test, and that it can achieve the same functionality as inheritance, I've pretty much stopped using inheritance. And I write Java 99% of the time.
But if you use UIKit, then it's fairly important to use classic MVC (not MVVM), as that is what the SDK was specifically designed for. Trying to coerce it into other models just causes a lot of extra pain and complexity.
Also, there are models that have been specifically designed (I won't talk about which), to introduce extra complexity. These are made to allow a design to be "broken up," so that parts can be assigned to different developers.
> and it won't change, so we need to behave like it won't...
I sincerely hope that you're wrong. It's been an unmitigated disaster.
In projects I lead I’ve been discouraging inheritance because I’ve found it highly detrimental to maintainability. “Base” classes invite bloat and overgeneralization, especially with inexperienced devs.
I haven't found much joy in MVVM. I haven't found that it provides enough benefits to spend the time switching over.
I am looking forward to SwiftUI. I really hope that the documentation improves, though.
If you haven’t checked out PointFree’s content (authors of TCA among many other things), it’s one of the best resources on Swift and algorithms for sure.
It is but it's quite straightforward to build your own simpler version using only Combine - that's what we're doing at the moment and it tends to work very well, interacting with Core Data has been kind of clunky though.
> I am looking forward to SwiftUI. I really hope that the documentation improves, though.
I've been on a project using it for the last 8 months both for macOS and iOS, I would honestly not rush into using it anytime soon. It's improved significantly since release, especially if you can target the newest iOS but requiring that is a hard-sell for most. On the Mac it's a different story and is much more neglected, macOS versions also tend to have far longer tails than iOS so it's a non-starter to try and sell a Monterey-only Mac app.
Edit: rereading my comment it sounds like I'm really down on SwiftUI - which I'm not, I think it's great when it works but there's a lot of rough edges at the moment and also quite a lot of hype.
That's a nonstarter, for me. I'm pretty picky about dependencies. If I use external ones, I like them to be something that can be encapsulated/injected, as opposed to being totally interwoven.
But I'm not trying to make money, I just want to write really good apps.
Even if only by a week or two, it can be enough to wreak havoc (but my experience is that they are often many months behind).
Like I said, though. I am not competing with anyone, or trying to make money. I write software for free, for those that can’t afford it. I pursue the vocation as a craft, and deliver Quality that would be totally unacceptable in a commercial endeavor (by which, I mean, high quality. Most commercial efforts go for “good enough” quality).
Most folks here would not find working the way I do, to be very satisfactory.
I mean, "prefer composition over inheritance" was in the GoF book which is from 1994, and is one of the core OOP books. If you've just been studying OOP for five years, it's likely that you weren't even BORN when this was already industry-level good practice.
With a lot of other techniques, if the implementation isn't perfect and you don't own the library you're using you still have a lot of power available to patch things up and make everything play nicely together. Inheritance mixes implementation details with your type checking, so in a lot of languages that can make it extremely painful to turn a body of almost-good-enough code into something that's actually usable.
The only code style advices that I've found to hold nigh universally are the following:
- The best code is no code
- Don't end classes in 'er' or 'or'
Coding paradigms are good when they let you do those things and are bad when they don't do both of those things (i.e. they result in more code or clases ending in 'er'; a class named 'Helper' is a code smell worse than sulfur dioxide)
So they pretty much all are "er" classes. ¯\_(ツ)_/¯
I'm not ruling out that this is an illustration of the problem with classes ending in -er.
It's just the way you write iOS apps (at least, the original design way).
It's classic MVC. Most of the action happens in the controllers.
BTW: I totally agree that the best code I write, is the code I don't write.
If needed I can delegate the specialization to an interface or in a function reference parameter (so anonymous functions can be passed) rather than in a subclass.
When implementing the interfaces I might use subclasses though, if I have some very similar variants.
Plain data structures with polymorphism via traits/interfaces/protocols seems like it's becoming the more popular way to handle these problems (I have no way to prove this, of course), and I prefer that way as well.
It's just the worst though when a dev sees two chunks of code that happen to be "shaped" the same and decide to tie them together with one abstraction. It's like seeing two different cables in a building that carry radically different signals but happen to go along the same path for awhile, and someone comes along and zip ties them together. You may have wanted to be able to route one of them completely differently, or maybe you wanted to add one more of the same cable, and now you have to cut up all the zip ties and either re-apply them (shove it into the existing abstraction), or bundle them up some other new way.
I had to do that, in the early '90s, because I was writing an SDK that had to be implemented in C, but used object abstraction. It ended up being used for over 25 years.
Once you abstract only to shorten your code you’ll likely regret it quickly.
Too many bad abstractions are how you quickly end up with that "Bad code base" that you believe is difficult to change - Things are wrong because they're tied together in ways that don't actually make sense, and changing code to support refactoring one use-case creates a wave of cascading changes to other places those abstractions are touched/consumed. If you miss one, or forget an edge case, or have a skimpy test suite - suddenly that refactoring you're so keen on is what's introducing new things that are "wrong" - because they shouldn't have been tied together but were, and you don't understand or remember all of the edge cases.
Basically - My rebuttal is this: It's very easy to refactor a codebase with duplication and introduce an abstraction for the current behavior. It's very HARD to refactor a codebase riddled with abstractions that shouldn't be there.
This means that by default - abstractions should only be introduced very carefully. Refactoring is fine, but you're paying more to refactor a bad abstraction than to refactor duplication. Good programmers understand that most of their value isn't in what their code looks like - it's in what it does for the users. Duplication can feel dirty, and it tends to trigger a "puzzle game" mentality in a lot of programmers, who want to fit the pieces together to make it pretty. AVOID THIS INSTINCT.
I've dealt with too many code bases that didn't follow this advice. It's extremely expensive to cut down capabilities from code because they are overly coupled.
DRY tends to create things like utility classes and deep dependency trees. For example, I saw a non-ui code base that pulled in JavaFX to use their pair class.
Delete all multifunctional programs.
Don't AMPlify code.
I find a mentality of _striving_ for DRY by default — even if end up choosing to duplicate for pragmatic reasons — to be beneficial in keeping programmers looking around for opportunities and refining the understanding of the context, with a better chance of incremental progress.
The same issue arises with YAGNI. People often jump too quickly to shouting YAGNI when the reality may be, and again this comes from experience-enabled judgment, that you are going to need it. "Don't make it a parameter, we aren't using it yet and don't know that we will." "But we do know, it's in the customer requirements that we don't hardcode the database server name everywhere, also it's just sensible."
These rules, principles, guidelines, laws, or whatever term gets assigned to them are there to help. They are not there to be excuses to stop thinking, but to provide a structure around thinking and a way to discuss with other people. But that structure is not absolute, experience and judgement can lead to breaking any of these rules at any time based on the present situation. That present situation that only you (and your team) know, but not the people who discovered, created, or coined the rules. They can only offer advice and guidance and not absolute instruction.
The focus on syntax misleads in two ways. First, it sometimes motivates consolidation of things that are merely coincidentally syntactically similar (pushing back on this is most of where WET and Ro3 are useful, when they are), which makes it harder to update the code when one "piece of knowledge" needs updating but not the other(s). If you have 10 things that all happen to be identical today, but any of them might change independently in any direction, combining them isn't "more DRY".
Second, the same piece of knowledge may be repeated in different syntax. If your setup means you have to say "there's a button here" one way in HTML, another way in JS, and another way in CSS, then combining those would be "more DRY" even though no syntax repeats. This isn't to say that combining those (say through code generation?) would necessarily be better - DRY is a guideline to be balanced against other guidelines; but I do assert that it is a more useful guideline when we think in terms of duplicated knowledge than in terms of duplicated syntax.
It goes along the lines of: W beats X, X beats Y, Y beats Z; in terms of what principle you'd like to apply to your code. One of these letters was essentially representing DRY. I summed things up pretty nicely. Does someone happens to remember?
>I try to optimize my code around reducing state, coupling, complexity and code, in that order. I'm willing to add increased coupling if it makes my code more stateless. I'm willing to make it more complex if it reduces coupling. And I'm willing to duplicate code if it makes the code less complex. Only if it doesn't increase state, coupling or complexity do I dedup code.
https://news.ycombinator.com/item?id=11042400
edit: and of course it's from a long and worthwhile HN thread on Sandi Metz's original article which was the start of the back-and-forth resulting in the article for this thread
This is indeed what I was looking for. I too was struck by it. Thank you SO much.