5 ms·
This is a very amateurish take The author very clearly (at least at the time of writing this) has not dealt with complex code bases. > If I were to see a confu
by cogman10 3y ago
This is a very amateurish take
The author very clearly (at least at the time of writing this) has not dealt with complex code bases.
> If I were to see a confusing piece of code littered with conditional logic, I wouldn’t see it and think “oh, there’s an incorrect abstraction”, I would just think, “oh, there’s a piece of crappy code”. It’s neither an abstraction nor wrong, it’s just bad code.
This is the primary issue. The author does not recognize that poor abstractions can involve more than just a lot of conditional logic. That sometimes, that conditional logic bubbles in places where secondary to where the bad abstraction was made.
A simple (real) example of this. One seen code where "get, these two objects share a field, let's pull out a base object and have them both inherit from it, after all, duplication is bad!". Then later on "hey, here's two other objects with the same field, but they don't have that old base objects field, duplication is bad, so let's make a third base class"
This sort of thinking resulted in a really gnarly object graph. But further, down stream code had to do type checks and casting to compensate for this bad abstraction.
All because the original dev didn't want to duplicate a field on two otherwise unrelated objects.
And worse, you the dev that works on this code years later are left with the option "keep it as is, of rewrite and touch 100s of files potentially breaking large amounts of code)."
Oh, not too mention the unit tests that accompanied such code, ironically, filled to the brim with duplication around this hierarchy making minor charges massive.
On smaller less complex code bases you rarely see this comedy/tragedy play out.
- feoren 3y agoClass inheritance is flawed because it tries to be two things at once: a shared "surface" (public members, polymorphism, etc.) and shared implementation. An abstraction is only a surface -- this could be an interface, a function declaration, or even a data model. It almost never happens that implementation-sharing and surface-sharing completely coincide, and this is why class inheritance is falling out of favor and something I completely avoid (occasionally I will use abstract classes, but I usually regret it later). This is where "favor composition over inheritance" comes from. I'd go so far to say that because they cannot be completely divorced from implementation details, base classes cannot even be called abstractions. So if "wrong abstractions" includes shoddy base-class shenanigans, then the statement becomes almost tautological. Of course duplication is better than class inheritance -- everything is better than class inheritance. So the real statement there is "class inheritance is actually awful", which is important to understand, but a side point to this debate. If you don't count class inheritance as abstraction, then the tradeoff between code duplication vs. abstraction becomes much more nuanced, and that's what all this discussion is about. I certainly don't agree that ignoring class inheritance is a signal that the author is amateurish. Many complex codebases have no class inheritance at all.
- jeremyjh 3y agoInheritance is very useful in domains like game engines, where it is very common to have a base object such as "Node" that has some properties that every object in the scene graph must have, which all share the same implementation. For example they should all have a parent property and a collection of children, and ways to modify those properties. They'll also share methods such as "render" which probably must be overridden in every subclass. Its not impossible to solve this with interfaces and composition but those solutions are sub-optimal. An example you might be more familiar with is the DOM of a web browser - every element has some basic properties and methods that all share an implementation.
- yellowapple 3y ago> Its not impossible to solve this with interfaces and composition but those solutions are sub-optimal. The growing popularity of ECS and data-oriented design in game engines suggests otherwise: keeping components separate from entities enables both performance enhancements and separations of concerns that are much more difficult to achieve with the traditional inheritance-based approach. To illustrate a bit: > it is very common to have a base object such as "Node" that has some properties that every object in the scene graph must have, which all share the same implementation. For example they should all have a parent property and a collection of children, and ways to modify those properties. You don't need subclasses for that; you just need a table of entity IDs (where both the things to render and the scene itself are entities) and parent IDs, which you can then recursively walk to get the entities you want to render: WITH RECURSIVE entity_children AS ( SELECT id, parent FROM entities UNION ALL SELECT ec.id, ec.parent FROM entity_children AS ec JOIN entities AS e ON ec.id = e.parent ) INSERT INTO scene_entities (scene, entity) SELECT $scene_entity_id, id FROM entity_children WHERE parent = $scene_entity_id; (Obviously you probably won't actually be running SQL queries in a game engine's rendering loop; this is just to illustrate the logic.) Once you've got that list... > They'll also share methods such as "render" which probably must be overridden in every subclass. You don't need subclasses for that; you just need a table of entity IDs and things to render, which you can then query and send to the GPU: INSERT INTO some_buffer_in_GPU_memory (entity, mesh, texture, position) SELECT se.entity, em.mesh, et.texture, ep.position FROM scene_entities AS se JOIN entity_meshes AS em ON se.entity = em.entity JOIN entity_textures AS et ON se.entity = et.entity JOIN entity_positions AS ep ON se.entity = ep.entity WHERE se.scene = $scene_entity_id; (Again: you probably ain't actually using SQL for this; this is also overly simplified, since most modern game engines use all sorts of other stuff besides a mesh, texture, and position when rendering something. Note also that "em.mesh", "et.texture", and "ep.position" need not be actual meshes/textures/positions, but could instead be indices into buffers already on the GPU.) The key advantage in both of these cases is that the parent/child data and the render data can live where they make the most sense, and can be processed by independently-running systems with minimal contention. This is critical for processing game logic in parallel - something which the game industry is learning the hard way with legacy engines that can't fully exploit multicore hardware.
- ozim 3y agoI was looking for a word to describe my feeling about the article and "amateurish" fits the bill. What mostly took me down was: (for example, the same several lines of code duplicated across distant parts of the codebase dozens of times, and with inconsistent names which make the duplication hard to notice or track down) It is silly example because in such scenario there is no way you can even start writing abstraction to handle that. Other part is what cogman10 wrote, wrong abstraction is not "simply piece of code gathering if statements". Wrong abstraction is piece of code or whole part of system where you cannot simply add an if statement and get going. Wrong abstraction might be something that actively prevents you from changing code in meaningful way. There is also another comment I would riff off about DevOps and having scripts per team/domain even if mostly those look the same you never know what the team will require. Nowadays domain driven development is in vouge, mostly because it recognizes separation of concerns is much more important than DRY. To finish off, author also assumes abstractions are born by de-duplication of code, yes we discuss "duplication is cheaper" so as finishing wanted to rant on something. Worst abstractions I saw in practice were born in heads of "Astronaut Architects" who built some system top down making stuff up "because it should be like that". Other bad ones were done by junior devs who were high on DRY.
- laserlight 3y ago> It is silly example because in such scenario there is no way you can even start writing abstraction to handle that. You start writing the abstraction the first time you duplicate, so that you don’t end up with this mess down the road.
- kiitos 3y agoBetter rule is the first time you duplicate, just copy/paste the code. But once you duplicate a third time, that's a pretty good signal that you have something that is common enough to be abstracted, and you can write the abstraction then.
- laserlight 3y agoIn my experience, one is unlikely to remember how many times a piece of code is being copy-pasted, unless they are doing it in the same coding session. Everyone copy-pastes, thinking that they'll DRY the next time and the codebase turns into a mess.
- em-bee 3y agoi wouldn't create a base class until there are a non-trivial amount of common properties shared by several classes and i find that i am adding more such common properties. and when a class appears that doesn't have one of these common properties, then perhaps it makes more sense to move that one no-longer-common property out of the base class back into the individual classes so that again i can have all classes share the same base class.
- bitblender 3y agoI think your disagreements are valid, but I don't think it is fair to say this is an amateurish take or infer the author's level of experience. Your example of unnecessary inheritance hierarchies (which I have also faced many times in real world scenarios) may even be a symptom of exactly what the author is saying: what you might call a "bad fitting abstraction" the author would just call "bad code". The implementation details of how code gets shared (composition vs inheritance) is a subtle but still vital consideration to the cost benefit analysis. The author is observing that it might be misleading or dangerous advice to urge developers to choose duplication just because issues with abstraction have been historically observed, which I completely agree with and do not consider myself to be an amateur. I also agree with you and other posts that the author fails to mention the (exponentially higher) costs of abstraction boundaries that also span human organizational boundaries.