Hacker News new | ask | show | jobs
by sevensor 696 days ago
Although I agree with the recommendations, I cringe at the definition of abstraction. In a sane world, abstraction doesn't mean defining classes so much as it means identifying important unifying concepts. DRYing your code by moving a method to a common base class isn't abstraction in any important way, it's just adding a level of indirection. In fact, I'd argue that this example is the opposite of abstraction: it's concretion. Now every subclass relies implicitly on a particular implementation of that shared method. Not that doing this is never useful, but it's a mistake to call it abstraction when it's nothing of the sort. No wonder people complain that their abstractions leak.
4 comments

I'm currently dealing with a codebase that does this to a ridiculous extent. Like, literally, every change affects the entire project because everything is made of base-classes mixed in weird ways. Every concrete object inherits multiple base classes and no individual behavior. Imagine something like this:

    class Book extends ShelfableItem, Pagable, Authored, Readable, BaseBook {}
It's absolutely insane.
Oof. I know this is an example but all of those look like they would be better served as interfaces.
Even if they were interfaces, it screams of the "model your code after physical objects" approach, where a system has 1 enormous "Book" type which represents all the things you can do with a physical book.

It seems unlikely that the same type should be "Shelfable" and "Readable" / "Pagable," because they describe distinct sets of operations. When a book is on a shelf, you can't page through it. If you "read" a book on a shelf, you only see the title, author, and maybe some pull quotes.

It depends. Of course we don’t see the whole picture because it’s just an example by OP, but I also find weird that the de facto solution to abstract classes is: interfaces. Sometimes, duplication is better than interfaces.
Yes. Did I mention there are interfaces too, with almost the same name, but it’s only used for the base classes. (Yes, there is only one implementation of all interfaces).
Having lots of interfaces for common things is not a bad thing. See how Rust traits work... even basic structs you create will probably implement lots of basic traits (some of which can be done automatically, thankfully) like `Display`, `Default`, several `From` or `Into` impls, `Clone`, `Copy` if your type is "light", `AsRef`, `Send` and many more!

This makes code much more reusable as so many functions are written based on those basic traits alone.

Of course, finding the right basic types is really hard and your company seems to have done that badly, but in principle, having some basic types to model very common "things" is a necessary thing.

The issue isn't the interfaces; it's that there is only one implementation (the base classes) per interface—so why even bother having an interface? Otherwise, I agree with you to a degree.

The main issue with the codebase is that if you want to, say, change the behavior of a Book, you have to go change the behavior of some base class (after working out which one is actually being called). This base class might be used in a Clock, Field, or Filesystem as well—something so conceivably far away that their similar behavior is a coincidence and not really related at all. Then you get to argue with the architect about whether "reading a book" is different than "reading a clock."

crying in rails concerns :*-(
Except usually the name of every type the Book inherits from or implements is so long it has to be put on a separate line.
If by abstraction you mean identifying unifying concepts then I cant understand how you reasoned yourself into thinking that identifying a common method and sharing it between multiple classes by the means of the super class is not abstraction. You have identified a commonality - the common code, common method. By your definition it's abstraction.
It's not abstraction because it's not presenting a simpler mental model. It's just shoving some code located somewhere else into scope. It's mere indirection.
Example of abstraction vs indirection:

If I said 'User', we both know exactly what that means. It's so semantically simple that laypeople know what it means. But our implementations could vary wildly. Someone who's just taken Java 101 will be thinking of a class with getName() and setName(). But someone who's just taken SQL 101 will think of a User as an INT or UUID, where features are added by referencing that user's id from different tables. User is abstract because it's understandable and not locked into any particular implementation.

I love Kafka. But it's a PITA to program against, at least in Java. I cannot code directly against it and always need to make my own wrapper classes to construct and poll it. I'll make ResumingKafkaReader and RewindingKafkaConsumerFactory, etc. These are not abstract, because they are very specific about what and how they do things. They are concrete behaviours wrapped with 1-2 levels of concrete indirection.

However, I might inject one of my Kafka indirections into a business logic class, interfaced as a Supplier<User>, which makes it abstract. I can then unit test my class, safe in the knowledge that my class cannot know if a User came from Kafka or just a test stub.

So I push back on the thesis of the article, and double-down on doing things abstractly first and foremost. This is closely related to the dependency inversion principle. Write (and test) your business classes around Users and other abstract things. Once you've done it wrong a few times and eventually gotten it right, then you can start writing the indirections (e.g. AbstractKafkaFactory) which the article rightly claims slow you down in the beginning.

I came to say more-or-less the same thing. The author is making some valid points, but the moral is that premature reification of abstract concepts may be harmful, especially if there is something vague about them.
I had to mull over this for a while, but I think I agree - abstractions at a conceptual level are much more powerful than object-level "compression".

Concepts/domain model/whatever tend to change over time though (at least in the business world, maybe not so much tooling etc). I think that's another source of leaky abstractions - things that conceptually made sense together at one point grow apart, and now you're left with common code that is deeply integrated but doesn't quite fit any more.