
Beginner's Guide to Abstraction - jesseduffield
https://jesseduffield.com/beginners-guide-to-abstraction/
======
chowells
What's the source of this idea that "abstraction" means "combining duplicated
code"? An abstraction is a transformation between models that strips away
information not essential to the target model. This is an idea firmly planted
in semantics, not code coincidence.

The first example in the article is a nice demonstration of this happening
well. You are transitioning from a model where geometry is unknown to one
where at least simple geometry is known. It may not be the most compelling
model change ever, but it at least captures the key point - there is a
semantic operation going on: calculate the volume of a sphere. The actual
formula for doing so isn't important at the level you want to think about your
code, so replacing the formula with a function call simplifies the model in
which you're working.

Contrast that with the example of things going wrong in the next part, with
the bad "average" function. What unnecessary details are being removed there?
Being sure you're calling that function correctly actually takes more work
than calculating an average without it. That's not an abstraction, it's an
indirection. You still need to track it down and read the code to understand
it. That's not something you have to do with sphere_volume in the preceding
part.

So how do you know whether duplicated code represents an opportunity for
abstraction? You start thinking in terms of the semantics you want to be
using. Is the code duplicated because it's doing the same thing, semantically,
in your target model? Well then that's an opportunity for abstraction. Or is
it just duplicated because it happens to share an implementation? Well then
that's just a coincidence. Don't try to share code.

Not to suggest that this is an easy test, of course. It's very possible that
two things might be different instances of a common problem that you're
unaware of, and you have no idea that they share code because they actually
are the same. That's ok, and there's always more to learn. But I think if
people put more thought into what abstraction is and why it exists, the
questions of when and how to use it fade away.

~~~
jesseduffield
In my mind, abstraction is finding a single representation for various things
that share traits. If those things happen to be chunks of code all performing
the same task in the same way, then factoring the code into a method would be
an example of abstraction. I'm not sure that this definition conflicts with
the idea of abstraction being about higher-orderness as another commenter
suggests, but I don't think it does.

I disagree with your claim that the 'bad' `average` method is an example of
indirection rather than abstraction. Indirection, afaik, is about decoupling
two things via an interface so that one isn't directly dependent on the
implementation of the other. In this case you are still directly calling the
method which will directly execute the code. Maybe the dispute here is around
whether we should call these things 'the wrong abstraction' or 'a failed
attempt at abstraction' but I think it's a spectrum where you can have a good
abstraction but with some parts that don't really belong, and in that case
it's still an abstraction.

~~~
daviddaviddavid
Not the parent, but I think one of the main differences here is that your
definition seems to imply that one _abstracts from code_ where parent's
definition seems to imply that one _abstracts from the domain_ which one's
computer program is modelling.

I'd tend to think that the "chunks of code all performing the same task"
aren't the thing you abstract from, rather they are just a poor abstraction of
the same domain.

~~~
jesseduffield
I agree with your distinction, however I'd say that it's abstraction all the
way down: the code you write is an abstraction of the business requirements,
which is itself an abstraction of the business domain, and when you're dealing
with legacy code that nobody understands, it ends up being treated as a
defacto domain unto itself.

So I don't think the term 'abstraction' should only be reserved for use in
reference to the domain because abstraction is already happening at every
level anyway.

------
codemonkey-zeta
I'm surprised I'm the first to bring this up, but the "over-abstract" example
feels more like "over-specified". The actual operation being defined works on
the same level of abstraction, since it defines precisely the same operation.
The first has just specified extra tweakable aspects of its execution via the
argument list. I'm not saying it's not bad, I just don't think it's because it
is "too abstract" compared to the simpler solution.

Abstraction in my mind is fundamentally about the "higher-orderness" of a
thing. These two average methods are just as abstract as the other, since one
is not a higher-order operation than the other. I would use the word over-
abstract if one was to write a program modeling a dog-walking business (a very
specific thing), by writing a system which models actions on entities (a very
abstract thing), where walking is an action which may involve one or more
entities, and dogs, humans, employees, and customers are all entities. If the
core thing you want to do is just a single concretion of the system that you
actually built, then you "over-abstracted". I feel like we should not
discourage the practice of abstraction, since that's our business. I literally
get paid to think about the real world in terms of abstraction and write it up
into a computer. Young engineers should not be taught to fear "over-
abstraction".

~~~
MaxBarraclough
> The actual operation being defined works on the same level of abstraction,
> since it defines precisely the same operation.

Agreed. This confusion is something Zed Shaw wrote about in blog post called
_Indirection Is Not Abstraction_ [0]. Surprisingly it seems it was never
discussed properly on HN [1], but it was discussed elsewhere [2][3].

(There's also another blog post by this name by another blogger, by
independent reinvention/coincidence [4].)

> Young engineers should not be taught to fear "over-abstraction".

Disagree. As you just showed, unnecessary abstraction is bad. Ineffective
abstractions are also bad. It's not easy to get right.

[0]
[https://web.archive.org/web/20160304022133/http://zedshaw.co...](https://web.archive.org/web/20160304022133/http://zedshaw.com/archive/indirection-
is-not-abstraction/)

[1]
[https://news.ycombinator.com/from?site=zedshaw.com&next=8820...](https://news.ycombinator.com/from?site=zedshaw.com&next=8820179)

[2]
[https://www.reddit.com/r/programming/comments/38hobm/zed_sha...](https://www.reddit.com/r/programming/comments/38hobm/zed_shaw_indirection_is_not_abstraction/)

[3]
[https://lobste.rs/s/ja5ihv/indirection_is_not_abstraction](https://lobste.rs/s/ja5ihv/indirection_is_not_abstraction)

[4]
[https://news.ycombinator.com/item?id=18344033](https://news.ycombinator.com/item?id=18344033)

~~~
gen220
Thank you for these excellent links. This debate often takes the concrete form
of “one large imperative function with 100 lines” vs “5 functions with 20
lines”. Many people who separate out logic for the sake of minimizing lines-
per-function unfortunately do so by introducing indirection, rather than
abstractions, and thereby make the program more challenging to reason about
and test. But, the reason the debate is never-ending is because the question
isn’t sufficiently defined! :)

I like to think of a program like a tree (main is root, each function is a
node, calls are edges). Each sub tree _should_ be a bounded context, in that
(ideally) you only have to think about parameters defined in that sub-tree.
Leaves implement the “nitty-gritty” (mainly I/O, number-crunching, and complex
transformations), and are heavily tested. Each node that _isn’t_ a leaf is
either an abstraction composing leaves, or an abstraction composing
abstractions. Unit tests for leaves test the nitty gritty, unit tests for non-
leaves must only test composition.

I find that human-readable modules have some limits (number of children,
height of the tree). You can violate those limits sometimes, but only if you
provide some assistance in the form of comments.

Sometimes, a 100-line function is _not_ composing many distinct nitty-gritty
ideas. It just really takes 100 lines to express “write this model to the
database”.

~~~
MaxBarraclough
I agree that relatively large functions aren't always an evil. As you say,
_sometimes_ there isn't a tidy way to further decompose it. At the same time
though I don't think it's always a sin to write a function just for
decomposition, without it doing any abstraction.

A 'helper function' might be tightly bound to some other function, i.e. the
helper function is sensitive to the internal workings of the function it
serves, and is not intended to be called from anywhere else.

And there's still no excuse for source files that are 5000 lines long, of
course.

HN discussion of John Carmack's thoughts on how long functions are sometimes
preferable:
[https://news.ycombinator.com/item?id=8374345](https://news.ycombinator.com/item?id=8374345)

------
kevsim
One of the nightmares I've experienced time and time again in large mature
codebases is incomplete abstractions. Some developer gets a great idea of how
to abstract something away, defines the plumbing needed for said abstraction,
and sets to work going through the codebase bit by bit, moving code over to
this new abstraction.

But then, they leave. Or they change projects or they just lose their
enthusiasm for this major refactor. And you're left with a half baked
abstraction in the code. Then another developer comes along, and another, and
another and before two long you've got a spaghetti mess of incomprehensible
"abstractions".

I was told by a friend once that the iOS app for Facebook has a whole bunch of
implementations encapsulating what a "form" is in the app. Many developers
came and went with their own ideas of what that abstraction should look like,
but none became the one abstraction to rule them all.

~~~
cooperadymas
I run into this scenario particularly in large React code bases. Multiple
developers building their own half baked abstractions on top of a framework
with its own continually shifting abstractions. Lack of uniting engineering
leadership and not enough time to devote to code discipline - it's a real joy.

~~~
searchableguy
My problem with react is that there are so many ways to do the same things
that reusability and refactoring becomes harder than it should be.

I usually found 3-4 ways of styling in a medium codebase on github. I find
varying degrees of state management - some built with context and hooks, some
imported and traditional mobx/redux. I haven't found many significant
opportunities to reuse code without refactoring a lot or doing micro files of
7 lines of code and separating logical components.

I now think of react as a library to _build your own frameworks_. It aligns
perfectly with the javascript ecosystem mentality.

~~~
randompwd
Just started looking at Spring Boot (w/Kotlin).

Never try it. A bazillion ways to do basic web service & web things and every
google search returns different methods.

Nightmare.

~~~
vips7L
Did you look at [https://spring.io/guides](https://spring.io/guides) ?
Building a REST service is one of the examples.

[https://spring.io/guides/tutorials/rest/](https://spring.io/guides/tutorials/rest/)

------
pierremenard
I've learned the hard way that perfect abstractions don't exist. The
mathematician in me wants to find the "most elegant representation" of a given
problem, but when I give in to that urge, I often end up with a god function
that takes `n` boolean flags that toggle the behavior slightly for different
cases.

Why does this happen? I think a partial explanation is in this Nietzsche essay
[1], where he says, "every concept arises from the equation of unequal things"
— in other words, the abstractions of the world were built bottom up in our
heads, and the Platonic essence of things is just a fairy tale we tell young
programmers so they can sleep easily at night.

1\.
[http://nietzsche.holtof.com/Nietzsche_various/on_truth_and_l...](http://nietzsche.holtof.com/Nietzsche_various/on_truth_and_lies.htm)

~~~
twhitmore
A key insight can be to abstract behaviors, rather than state.

It is typically much more possible to cleanly abstract the behavior for a
single interaction/ role, than for the entirety of an entity's state. If you
have multiple interactions you might want to use more than one interface.

Once you have clean behavioral APIs for your interactions, you may also be
able to use composition, wrapping and delegation to implement/ enhance
behavior. This is the 'Strategy pattern'.

The one thing you lose here is object identity -- you can no longer assume a
delegate is the entity itself. This is no big loss given the clarity &
flexibility that are gained.

------
mplanchard
There was a great discussion here on HN a few months ago about DRY [0] and how
developers generally get the concept wrong. Specifically, it’s not about
removing duplicated code. It’s about ensuring there’s a single source of truth
for a given piece of knowledge in your application. For example, if you’re
calculating interest on purchases all over the place and hard coding the rate
everywhere, it should be unified into a single function or method, so that
there’s one source of truth for calculating interest. If you just have some
code that looks similar but doesn’t represent “knowledge” that is being
duplicated, DRY does not apply. Some folks in the comments mentioned they
liked to use SPoT as an acronym that’s a bit clearer, and I’ve been using that
since then in code reviews.

[0]:
[https://news.ycombinator.com/item?id=22329787](https://news.ycombinator.com/item?id=22329787)

------
foxtr0t
I think this definition of abstraction is lazy. Abstraction is indeed hard to
define precisely, but it may be more accurate to describe it as the process of
implementing interfaces that are conceptually familiar to users, often through
metaphor, such as the unix "pipe".

Creating the "right" abstraction is _not_ the process of bundling up repeated
code and only questioning how "abstract" it should be, it is the process of
creating an interface that is familiar and conceptually easy to grasp. This is
done through naming, comments, the use of metaphors, etc. From this viewpoint
there can be many correct levels of abstraction, some more useful than others.
We should aim to create good abstractions at all layers of code, even if
finding abstraction bliss is unattainable.

~~~
kqr
As Dijkstra put it in the '60s: we start with hardware, which is fully capable
of solving our problem, but not designed to make it convenient to express that
solution.

So we make "virtual machines" by creating new "instructions" that extend the
physical machine. (Dijkstra called them virtual machines; these days we might
say APIs or something.)

We keep doing this: extending level n-2 into level n-1, with the guiding
principle that n-1 should be a "virtual machine" that makes it slightly more
convenient to express level n. At some level k, the solution to our original
problem is trivial.

Other important properties of these abstraction levels is that

\- They should do resource allocation and management to the point where a raw
resource used by level n should not be visible as such in level n+1.

\- Any level should ideally only depend on one level below it.

\- No level can ever depend on a level above it -- this creates cycles in the
dependency tree and prevents further extension and contraction of
functionality.

\----

Drawing further from Parnas instead: abstractions should hide design decisions
that are likely to change. Typical examples of those are the format of data,
layout of data structures, implementation details.

Things that are less likely to change are things that come from the problem
domain. Design interfaces based on problem domain concepts, not solution
domain concepts. This applies at every level: the abstractions of level n-1
should have interfaces in terms of problem domain concepts of level n.

~~~
cinnamonheart
One of my favourite quotes on the topic of abstraction comes from Dijkstra,
too.

> The purpose of abstraction is not to be vague, but to create a new semantic
> level in which one can be absolutely precise.

~~~
kqr
Yes! And this also ties in to the notion that abstractions should encapsulate
invariants.

------
hackeryogi
Well written article

> DON'T BE AFRAID TO DISMANTLE THE WRONG ABSTRACTION

Couldn't agree more with the statement, though I don't completely agree with
the author's suggestion to copy paste. Duplicating code _is debt_. It may help
us go faster now, but it'll almost inevitably come back to bite. It is
manageable if 1/2 people do it 1/2 times - definitely not manageable if 5/6
people do it 5/6 times.

I believe the general hesitation of not touching a piece of code (or, getting
by with that optional param) is due to the fear of fucking things up. Having
your code test covered gives an amazing amount of confidence to rip apart old
abstractions to yield newer ones that serve the purpose of the _current code_.
To me, this route is more preferable to duplicating code.

Even with the best of intentions, Hacking an abstraction with that one
optional parameter is inevitable. Tests help in our ability to repay that debt
faster - on time & in full.

Basically they make all abstractions a lot cheaper - easier to write and
easier to throw away. Thereby solving the problem of having a 'wrong
abstraction' too early.

~~~
sagichmal
It is almost always better to copy/paste a function to accommodate "that one
optional parameter" that breaks the original abstraction, than to add the
parameter to the function signature. The "cost" of a broken/leaky abstraction
is at least an order of magnitude higher than that of duplicated code.

------
Ozzie_osman
> Why is it a good idea to abstract the formula for a sphere's volume into its
> own method? Because if mathematicians ever found out they got the formula
> wrong, you would want to go through all the places in your code that you
> used the formula and update it to be correct. That is, we know ahead of time
> that we want the code to be in lockstep.

This is actually not the main reason you'd want to abstract, and I think the
whole article kind of gets it wrong. The main reason to abstract is not to
keep code DRY, but to "abstract away" things that are not important in a
certain context (or layer). You want to put the formula for the volume of a
sphere aside when it's not relevant to what the code at hand is doing and
would get in the way of trying to change or understand that code. For example,
a really strong case for abstracting that formula is if you're writing code
that is calculating the volume of several shapes.

Yes, code duplication is often a sign that you've screwed up your abstractions
(ie they correlate), and creating the right abstraction will often make your
code more DRY, but it's the means, not the end. The end is code that is easy
to read, understand, and change.

My high-level "sniff test" is essentially rubber-ducking (try to explain to
yourself, someone else, or a duck) what the code is doing. For instance, you
might explain a function called calculate_remaining_space_in_box:

1) we get the volume of all shapes (including spheres) in the box

2) we get the volume of the entire box

3) we calculate the difference between them

In that explanation, you realize there's really no extra benefit to a reader
of that code at that level to knowing the exact formula for the volume of a
sphere (or any shape for that matter).

There are, of course, other signals to measure whether you've abstracted
correctly beyond just code duplication. For instance, and the Single
Responsibility Principle is a good example
([https://blog.cleancoder.com/uncle-
bob/2014/05/08/SingleRepon...](https://blog.cleancoder.com/uncle-
bob/2014/05/08/SingleReponsibilityPrinciple.html)). Code that changes for the
same reason should usually be grouped together, and code that changes for
different reasons (or at different frequencies) should be grouped apart. But
again, this is in service of the end goal: making code easy to read and
change.

------
bokwoon
One of my favourite abstraction advice comes from this article:
[https://blog.carlmjohnson.net/post/2020/go-cli-how-to-and-
ad...](https://blog.carlmjohnson.net/post/2020/go-cli-how-to-and-advice).

"You want one layer to handle user input and get it into a normalized form.
You want one layer to do your actual task. And you want one layer to handle
formatting and output to the user. Those are the three layers you always
need."

The "do the task" layer can be abstracted again further. But starting it off
as a monolithic layer, separated from input and output, is always the right
call.

------
xcskier56
This is the sort of article that I could have really used at about year 1.5 of
my programming career. I’ve learned many of these lessons the hard way and
resonate/agree with the examples here. It would have been really nice to have
read this years ago and not have to hit my head on quite so many sharp corners
to learn.

You invariably have to hit hour head sometimes, but I hope clearly written
articles with understandable but not completely contrived examples like this
one reduce the head knocks for some people

~~~
darkteflon
I came here to say exactly the same thing. This is such an important lesson to
get early on - even at the very beginning of your career - even if you don’t
fully understand it until later.

------
l0b0
Nicely put! Most best practice articles end up reading like dogma because they
only ever show clear-cut cases where the best practice applies. Augmenting the
plain good/bad examples with _good /bad depending on the situation_ examples
seems like a great way to avoid that.

------
halayli
IMHO one of the best indicators that I've nailed the abstraction is when I am
able to use/re-use it in many places.

A concrete and easy example is when you're developing a new software and
building your common utils/libs. If you've nailed the abstractions, you'll
notice that you're able to expand the libs by frequently reusing/leveraging
other libs you've built.

Abstractions come to exist from the requirements. IMO the key to a good
abstraction is to be able to dissect a requirement into smaller requirements
that you are familiar with and have already solved and have a solid
understanding of them.

Metaphorically speaking, bad abstractions are ones that converts a requirement
into a new polygon shape, and good abstractions are ones that dissect a
requirement into one or more shapes that we are all familiar with (circle,
square, rectangle, rhombus etc..).

The difference between a polygon and common shapes is that no 2 polygons will
look alike unless the requirement is exactly the same and I'd argue that a
developer will create a new polygon if asked to solve the requirement twice.

When a new requirement comes in, it's common to start implementing it right
away. Create one or more classes with names that maps to the requirement, add
few methods etc and voila you have a new polygon shape.

The key point here is to dissect the requirement into sub-requirements that
look like familiar shapes (problems you've previously solved). Every once in a
while you'll end up creating a polygon here and there for a sub-requirement,
which get refactored over time to a known shape.

A good developer can quickly see the familiar shapes that the requirement is
hiding behind instead of creating a new polygon shape.

------
searchableguy
This article is good (although they could work on examples a bit more). One
thing I have really found useful when working with elixir is that it gives you
a way to abstract common patterns or add more use cases by differentiating
based on arity and pattern matching.

it's easy to write the average function in the article in this way.

    
    
      def average(arg) when Enum.all(arg, &is_string/1) do
      # implementation
      end
      
      def average(arg) when Enum.all(arg, &is_integer/1) # for the int
    

which would be a more proper abstraction as it hides details of the type of
your data.

or using pattern matching in the arguments.

    
    
      def shape("circle", ...) do
      # implementation
      end
    
      def shape("square", ...) do
      # different implementation
      end
    

When you have map as an argument, you can do

    
    
      def is_good(%{ hn: HN }) do
        IO.puts "#{HN.someprop} is good"
      end
    
      def is_good(%{reddit: Reddit}) do
        IO.puts "#{Reddit.someprop} is bad"
      end
    
      def multiply_on_two_numbers_otherwise_square(a), do: a * a
    
      def multiply_on_two_numbers_otherwise_square(a, b), do: a * b
     

My examples are trivial but this really gives you some awesome refactoring
powers.

------
imvetri
I want to share few things I learnt while making
[https://github.com/imvetri/ui-editor](https://github.com/imvetri/ui-editor).

It abstracts component development for frontend hiding details about
framework.

I applied DRY principle on code that we write. Framework syntaxes are a
repetitive hardcode that I tried to abstract.

Pragmatic programmer is the book referred to me by a friend of mine and it
definitely works.!

------
deltron3030
Kinda sad that we can't focus on "concrete designs" and have to deal with high
costs of rewrites and therefore "manual organization" and finding those
abstractions. If rewrites wouldn't be costly it just wouldn't make much sense
to compose an architecture manually.

Parametricism is slowly taking over other industries like architecture and
industrial design. In essence it's automatic rewrites and programs finding the
right abtractions/compositions/organizations based on given parameters, where
the designer job is more in the actual problem domain, providing the right
paramters to the program and selecting the most promising outcomes of that
automated process.

The web moving to site generators and serverless is maybe a glimpse of a
future with dynamic site generators, where the generators then get much
smarter and responsive to input parameters and surrounding contexts.

------
rahulmax
I'm a designer. Just came here to say, this is so much inline with the "right
level of abstraction" in graphic design:

[https://computersciencewiki.org/index.php/File:Abstract_hear...](https://computersciencewiki.org/index.php/File:Abstract_heart.png)

------
kovac
I'd argue that your definition of an abstraction resembles wrappers (which
IMHO is the weakest form of abstraction) rather than abstraction in general. I
think it's better to take it as modelling a complex system in a simple way to
solve some specific problem by stripping the unnecessary details. For example,
design patterns are abstractions but I don't think they all qualify as simply
collecting a larger interface into a smaller one. Similarly, a virtual machine
process like JVM is an abstraction for the hardware details which is also a
lot more than simply reducing the size of the hardware interface. Still, a
useful article. Thanks.

~~~
specialist
Agreed. Pretty good, worthwhile article. But it's about code construction and
organization, spanning abstract data types (ADTs) and object-oriented design
heuristics.

[https://en.wikipedia.org/wiki/Abstract_data_type](https://en.wikipedia.org/wiki/Abstract_data_type)

[https://www.oreilly.com/library/view/object-oriented-
design-...](https://www.oreilly.com/library/view/object-oriented-design-
heuristics/020163385X/)

------
luord
In the notices example, I arrived at number one for answer because of a
simpler evaluation: it's less code.

One of the principles I follow is "the best code goes unwritten" so a good
rule of thumb for a good abstraction, for me, is if it reduces (noticeably)
the total amount of code. Conversely, if abstracting just increases the total
code, I call it an indirection and avoid it.

------
mlthoughts2018
One hard fought lesson I’ve learned over the years is that copy/paste is often
a very good solution. If the downside is that a developer has to manually
spray a change to 50 different locations with the same copy/pasted
implementation snippet, that’s really fine. Even for 500 or possibly 5000
locations is fine, with a good editor available or other tooling. Testing
these changes for correctness is easy.

Meanwhile the cost for getting an abstraction wrong is often far worse. And
it’s really easy to get wrong because abstractions by their nature are always
built based on yesterday’s data plus good intentions. People are arguing about
subjective theories and unmeasured concepts of extensibility, wasting time
arguing about Liskov Substitution Principle, SOLID, dependency injection, type
system design patterns, etc., but it’s mostly junk that just adds code bloat.

Obviously there are other solutions that don’t require premature abstraction
or copy/paste 1000 times (like simple module functions as the core unit of
reusability, or a macro system for code injection). But the point is
copy/paste gets a bad wrap. It’s simple, straightforward, easy to automate,
easy to test, and adds no extra concepts to the code.

“Functional core, imperative shell” is the best advice I’ve found. Ruthlessly
avoid object orientation, and when you need it, stick to extremely shallow
inheritance. Make everything a module function, and when you write data
structures, don’t give them member function logic for their core functions
(like search, sort, add items, remove items), rather create functions that
accept data structure instances as arguments and perform these operations with
no class-like internal state.

~~~
tigershark
No, having to change the code in 50 plus locations is not fine at all, it’s
criminal. In a team there will be always someone that misses some of the
locations and the implementations will inevitably diverge over time. And at
that point every time that anyone needs to do a change he will need to
manually fine tune each of the implementations and test each of them since
their behaviour may well be different.

~~~
mlthoughts2018
Your concerns (missing locations and divergence over time) are fairly easy to
address as copy/paste scales. Of course there are downsides, just as there are
major downsides to solving with abstraction, it just comes down to a tradeoff.
Many times copy/paste wins that tradeoff but is refused because our industry
has a cult obsession with abstraction.

~~~
tigershark
How do you address them fairly easily?

~~~
mlthoughts2018
Various editor tools, code quality tools like sonarqube that can be configured
to track duplications, writing code generation tools (usually around
templating or macro libraries), and heavy coverage of unit testing.

You can do “test once, reuse everywhere” without any inheritance at all. You
could use metaprogramming, sealed types with pattern matching, or macros/code-
gen. It’s just tradeoffs to decide between them.

The point isn’t that creative abstraction _never_ wins that tradeoff, rather
just that it wins very infrequently compared to how often it is preferred
based on parochial “software design” hand wavy reasons.

~~~
tigershark
So you will need external tools to find duplication and edit all the locations
hoping that everyone in your team does the same. In the meantime instead of
having 30 lines that solve the problem you’ll have 30*50=1500 lines spread
everywhere that do exactly the same thing. With a proper abstraction you can
just look at one line and understand what it does, with your solution you need
to look at 30 lines for 50 times. And all this because you are incapable or
too lazy to find a proper abstraction? I worked on projects quite big with a
lot of copy and paste and only who never had this “pleasure” can be in favour
of copy and paste. Code generation, meta programming, macros are all tools to
avoid duplication not to allow you to copy and paste multiple times.

------
jyriand
In a perfect world all my abstractions would be small and self-contained
packages, that i could import as a library without any dependencies.

------
bvrmn
My first advise for beginners is not to start a new code with classes. It
allows to see shared state patterns after they build some data model and
extract it to real abstractions.

