If Inheritance is so bad, why does everyone use it?
buttondown.email
buttondown.email
The word "prefer" is critical to understand. It just means "usually choose A over B" not "B is never the right answer."
Unfortunately, since we - as an industry - like hard and fast rules, we move towards that second explanation and act like inheritance never makes sense. Like any tool, there's a time and place where it is the best tool, other times where it's a reasonable tool, and other times it's a terrible tool.
Therefore "everyone uses it" because either a) it's a reasonable approach or b) the developer doesn't know of or can't use a better one.
We should work on fixing b) instead of denying a) exists.
Where I think inheritance works best is when the state in base classes is limited and the interface is quiet clear. Ideally where you are meant to override is also well defined.
Where it's the worst is when someone tries to use inheritance because they notice coincidentally duplicate code. The worst hell for this is in application configuration. I've seen 5 layer deep inheritance trees to handle really basic things like "what port should this app bind to". It saved no code and introduced a bunch of complication around the transitive dependency baggage it brought on board.
IMO, configuration should always be done via composition. It's more than fine to have a bunch of smaller composable config pieces just so long as you can easily jettison the broken parts.
What benefit is inheritance providing here? What you described sounds mostly like a struct, at which point the only value the interface provides is possibly some computed fields.
The best example of this (IMO) is how `AbstractMap` works in Java. [1]
In order to make a new object which implements the `Map` interface, you just have to inherit from the `AbstractMap` base class and implement 1 method, `entrySet`. This allows for you to have a fully compliant `Map` Object with very little involved work which can be progressively enhanced to provide the capabilities you want from implementing said map.
This comes in handy with stuff we've done when you can take advantage of the structure of an object to get a more optimal map. For example, a `Map<LocalDate, T>` can be represented in a 3 node structure, with the first node representing the year, the second the month, and the final the day. That can give you a particularly compact and fairly fast representation.
The value add here is you can start by implementing almost nothing and work your way up.
[1] https://docs.oracle.com/javase/8/docs/api/java/util/Abstract...
I think that in effect, as you associate more behavior with a particular struct(as opposed to what you're attempting to do with said struct), the greater expectation it presents that the struct is what you code around. More and more gets added to state over time, and more expectations about behavior get added that don't need to exist.
Sure, you could say "Well, then just be strict about what behavior is expected in the interface"- but that effort wouldn't be necessary if we didn't make the struct the center of the behavior in the first place.
Used to think that way, but I now prefer the alternative - passing function(s)/lambda(s) for the necessary functionality of the dependent class.
This way is actually more flexible, as you can change behavior without modifying the override or having a bunch of switches/if-else in your required function.
So instead of 'entrySet' being defined inside MyClass, you would define it outside it, or possible as a static method, and pass it to AbstractMap when you create it.
So you don't need to have every class implement a bunch of interfaces like or Hashable, Orderable, etc. in order get the desired behavior.
Now I guess you would come back about you shouldn't be able to able to do that outside the class, but I also think those are also bad ideas. Python famous gets away with not having private/protected (although there is a way you can kinda of get something similar).
I think it is fair to say that this is idiomatic Java, and for that reason it is a great example within the context of Java.
But does it translate to the abstract? Given your hypothetical ideal language that allows you to do anything you can imagine as you can best imagine it, is this still a good example, or would you reach for something else (e.g. composition)? Why/why not?
You never strictly need inheritance, but strict need isn’t the criteria for whether something is a good solution (otherwise the fact that most things in programming can be done multiple ways would mean that almost nothing is ever a good solution, since there is almost always an alternative route to the same result.)
What you want to reach for to achieve compositional behavior or polymorphism are type classes, traits, and interfaces. They attach methods to data without the silly "Cat is an Animal, Dog is an Animal" cladistic design buffoonery.
There's nothing wrong with OO, except for inheritance-based OO.
[1] Don't get me started on multiple inheritance. Instead of solving problems, they invented them.
Grandparent->mutateCancerChancePercent("+|-", 3);
Parent
Child
By banning access to GreatGrandparent's getCancerChancePercent(); method, I won't know what the starting chance was, which will make it harder to determine nature vs. nurture. Isn't that what ancestry and genome mapping is doing? Going back up the tree?
Now, A human can create a child, but only if the parent object was a "Human." Am I thinking about this in the wrong way?
Composition uses a lot more indirection. That's bad on modern CPUs. Pointer chasing throws out performance, so composition is not always preferred, either.
In C++ there is extremely little difference between the code granted for inheritance or composition (unless you use virtual inheritance), so I have no idea what overhead you are talking about.
This has been somewhat mitigated in newer versions of runtime.
Not sure what it's like in JVM world, I know they had a better dervirt for a while.
JIT devirt can be nitpicky, PGO has improved the situation greatly but until around 6.0 or 7.0 devirt was easy to 'break' in a lot of common scenarios.
Heck, tying into the generic composition bit, ironically static generics still have issues right around devirts. Go figure.
For more complex composition, an ECS runtime would likely group all components of the same kind into the same place so that systems that ran with those components get even better data locality.
The GoF book itself is full of examples that use inheritance, but somehow the bumper-sticker-sized advice has taken on a life of its own.
I vaguely recall Riel's Object-Oriented Design Heuristics [1996] shared the same advice. https://www.amazon.com/Object-Oriented-Design-Heuristics-Art... https://www.oreilly.com/library/view/object-oriented-design-...
Aside: Young me obsessed over design patterns, methodologies, software engineering, SQA, etc. I had all the books. I started a design patterns study group and hosted it for a while. Now all that "wisdom" is just a bunch of old books. Somehow craftsmanship, me, or both became irrelevant.
Or maybe it was never important.
Like Alistair Cockburn opined about the failure of CASE tools, people somehow manage to ship software successful, without the benefit of all that smart stuff.
The C++ and Python code I see daily is fully inheritance focused, and I would like to understand how it could be done differently (or better) starting from a perspective I understand.
I even have an AggregateDrawObject class for cases where I need to run two animations at once for a single object.
You can do something similar with UpdateObjects too and have composable behavior.
Note that I still use inheritance, but just don't let it get out of control. Anyone saying "inheritance is bad" is either a moron or really means "deep inheritance hierarchies are bad" or maybe "bad designs are bad".
(1) There is a concrete set of cases where A is better than B. (2) But, there is also a concrete set of cases where B is better than A. (3) And, either (a) the cases where A is better than B are more common than the reverse or at least (b) people in the environment where the advice is developed are currently choosing B in cases where A is significantly better.
But, critically, “prefer A over B” does not communicate the set of cases where A is better than B or vice versa, so it tends to lead to the conditions where “prefer B over A” becomes the advice based on (1) and (2) being unchanged but (3)(b) working in the opposite direction.
For me the main point is (runtime) polymorphism. E.g. you have a function that takes a general type, and you can pass multiple specific types and it will do the right thing. And if you want to avoid huge if-else statements, you should put the code for the special cases in the classes, not in each function that operates on them.
You can get this without implementation inheritance, it is also possible to just have something like interfaces. But I do find it very convenient to put common code in a base class.
Not without casting. qsort is still:
void qsort_r(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *, void *),
void *arg);Inheritance is what will bite hard when hand-rolling OOP in C. For one, you can forget about the compiler enforcing substitutability and co/contravariance for you.
I think that's the ultimate culprit in everyone hating inheritance. If it weren't for Java, I think we'd all have a healthier view of OO in general.
I learned OO with C++ (pre C++-11), and now I work at a Java shop, but I'm luck that I get to write R&D code in whatever I need to, and I spend most of my time in Python.
In C++ and Python, you get to pick the best tool for the job. If I just need a simple function, I use it. If I need run-time polymorphism, I can use it. If I need duck-typing I can do it (in Python).
Without the need for strict rules (always/never do inheritance) I can pick what makes the best (fastest? most readable? most maintainable? most extensible? - It depends on context) code for the job.
Related to TFA, I rarely use inheritance because it doesn't make sense (unless you shoehorn it in like everyone in the threat is complaining about). But in the cases where it really does work (there really is a "is a" relation), then it does make life easier and it is the right thing.
Context matters, and human judgement and experience is often better than rules.
Everything is an object so there's no if-it's-an-object do this but if-it's-not-an-object do that.
Those first two you can do in (modern) Java. The third is a mess to be avoided at all costs. Interfaces and lambdas will cover most reasonable use cases for polymorphism.
https://www.cs.virginia.edu/~evans/cs655/readings/smalltalk....
Dynamic dispatch is implemented under the hood with lookup tables and function pointers. Sometimes, it is nice for a language to wrap a fiddly thing in a more abstract structure to make it easier to read, understand, and write.
The runtime part is what I dislike. If I have a fruit which is an apple or a banana, I can't pass that to a method expecting an apple or banana. It can only be passed as a fruit.
> And if you want to avoid huge if-else statements, you should put the code for the special cases in the classes, not in each function that operates on them.
This is common OO wisdom that I strongly disagree with. For example, in my program I have a few types (Application, Abstraction, Variable, etc.), and a lot of transformations to perform on those types (TypeCheck, AnfConvert, ClosureConvert, LambdaLift, etc.).
I prefer to have all the type-checking code inside the TypeCheck module, and all the closure-converting code inside the ClosureConvert module. I'd take the "huge if-else" statements inside TypeCheck rather than scatter typechecking across all my datatypes.
You can by overriding the method on apple or banana. If your method is on some other object, then yes, you cannot do this unless your programming language supports multiple dispatch.
There are ways to have your cake and eat it too, here, at least in some languages and patterns.
For example, in Go you could define "CheckType" as part of the interface contract, but group all implementors' versions of that method in the same file, calling out to nearby private helper functions for common logic.
Ruby's open classes and Rust's multiple "impl" blocks can also achieve similar behavior.
And yeah, sure, some folks will respond with "but go isn't OO", but that's silly. Modelling polymorphic behavior across different data-bearing objects which can be addressed as implementations of the same abstract type quacks, very loudly, like a duck in this case.
It's less "this is not OO" and more this is not inheritance, which is why a lot of people are saying you can find more elegant solutions (like this) rather than use inheritance for no clear benefit.
Heh? An apple is a fruit, you can pass it to any place expecting the former. Like, this is Liskov’s substitution’s one half.
With generics, you can be even more specific (co/in/contra-variance).
Fruit* fruit = new Apple();
ConsumeApple(fruit); // Doesn't work; requires Apple*
fruit = new Banana();
ConsumeBanana(fruit); // Doesn't work; requires Banana*
ConsumeFruit(fruit); // Okay, function signature is void(Fruit*)If OP wanted to be allowed to do whatever and just have the software fail at runtime, JS is right there.
I would never. I know that I'm holding a banana, I want to pass it to a method that receives a banana. But what I can't do is put my banana through a rotateFruit function first, because then Java will forget that it's a banana and start treating it as a fruit.
It's pretty much the definition of OOP.
The core feature of OOP is just bundling functions with the state it processes.
When you bundle state and functions together, you can't predict what calling the function will do without knowing both the code and state.
You can say its 'the real benefit', I guess, but that feels like circular reasoning. Its pretty much the definition of what OOP is, so calling it a benefit feels weird.
Unfortunately, designing systems as a collection of distributed state machines tends to become maintenance nightmare. Functions and data being separated tends to make code better, even when working in so-called 'OOP' languages.
It's a benefit compared to how people were writing code before it became mainstream (for polymorphism: by jumping to data-defined parts of the code and hoping for the best, and for encapsulation: subroutines working on global variables).
Inheritance forces you to define the world in terms of strict tree hierarchies, which is very easy to get wrong. You may even do a great job today, but tomorrow such properties don't hold anymore.
Regular composition allows the same functionality without making such strong assumptions on the data you are modelling.
No, it doesn’t.
Inheritance is the outcome of deciding to model some part of the problem space with a tree hierarchy (that potentially intersects other such heirarchies). It doesn’t force you to do anything.
I suppose if there was a methodology which forced you, as the only modeling method, to exclusively use single inheritance, that would force you to do what you describe, but…that’s not inheritance, that’s a bunch of weird things layered on top of it.
But you don't actually care about runtime polymorphism here. You care about polymorphic behavior, which can be implemented in a much more composable way with parametric polymorphism.
As another example, the Unix file interface (open(), read(), write(), flush(), close()) etc. is an example of runtime polymorphism, where the file descriptors identify objects with different implementations (depending on whether it’s a regular file, a directory, a pipe, a symbolic link, a device, and so on).
All operating systems and many libraries tend to follow this pattern: You can create objects to which you receive a handle, and then you perform various operations on the objects by passing the respective handle to the respective operation, and under the hood different objects will map to different implementations of the operation.
Yes you can. that's the whole point of type classes.
For example in c++ std::function exhibits parametric polymorphism without templates.
I'm not saying that I like it everywhere; IMHO it's a tool to be used sparingly and not with deep hierarchies. But it's not reasonable to avoid it 100% of the time because we're soiling ourselves over the thought of possibly encountering the diamond problem.
This is evaluated at runtime, thus giving me the runtime polymorphism, but doesn't make me muck with any kind of taxonomy trees. I can also add my own methods that work with the dispatcher, without having to modify the original code. I don't feel like it's any less convenient than inheritance, and it can be a lot more flexible. That said, I suspect it performs worse, so pick your poison.
> For me ...
Sorry, but right off the bat this is just painting you as an example of "There are N camps, and each camp declares the other camp as wrong."
Yes, that's what inheritance is for you. For others it is something else. Why is your way the one that "correctly understands" it?
The article itself covers this - that some languages have lumped 3 different concepts into one that they call inheritance, leading to the different camps and comments like yours. Your camp is specifically mentioned:
> Abstract data type inheritance is about substitution: this thing behaves in all the ways that thing does and has this behaviour (this is the Liskov substitution principle)
I feel people don't understand what inheritance and (object orientation in general) is useful for, misuse it, and then it gets a bad reputation. It's not about making nice hierarchies of Cars, Fruits, and Ovals.
Agree 100%. It starts from the earliest programming course where we just teach it all wrong; way to abstract (no pun intended).One point to add to yours: well executed OOP allows for "structural" flow of control where the object hierarchy, inheritance, events, and overrides allow for the "structure" of the hierarchy to control the flow of logic.
I wrote two articles on this with concrete examples and use cases:
https://medium.com/codex/structural-control-flow-with-object...
https://medium.com/@chrlschn/weve-been-teaching-object-orien...
I think the first part of the first article immediately introduces an anti-pattern - forcing the user to make an instance of a class just to be able to call a pure function. It's just unnecessary noise, either make them static methods, group them in an object literal, or import the module as a namespace.
Adding the "pattern" as a mutable public field is a bit sketchy, and would make it show up in intellisense, but hopefully nobody will access/modify it. Making the pattern as a `const` at module scope solves that problem but you handwave away that approach saying it "pollute the symbol space for intellisense", which isn't true (non-exported items aren't available outside the module).
The next section on validation is a good example of another anti-pattern. The example of:
public get isUserValid(): boolean
is not a good way of doing it, because it relies on hopes and prayers that the user of the class remembers to call this. A better signature would be: function isUser(input: unknown): input is User
Notice the key difference - you can't get an instance of User without it being valid. There shouldn't be a notion of "yeah I have a User, but I don't know if it's a valid User". Your way allows people to operate with User, blissfully unaware of whether it is valid or not, hoping that they might notice the right method to call. Instead: Parse, Don't Validate [1]Note that to do this correctly, you have to either:
1. Separate data and functions 2. If you must use a class, then hide the constructor and provide a static constructor with a return type that indicates the function can fail
[1] https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
_________
Part 2 was an interesting exercise in how the popular OOP patterns from the GoF book vastly overcomplicated code, negatively impact readability, and make following the code feel like Mario (the princess is in another castle)
No need for inheritance, abstract base classes, of any of that complexity, all of that could be done with:
type ShippingCalculator = () => number
const shippingCalculators = {
USPS: calculateUSPS,
UPS: calculateUPS,
FedEx: calculateFedEx,
DHL: calculateDHL,
} as const satisfies Record<ShippingMethod, ShippingCalculator>
Intellisense works fine of course, and code navigation (via F12) is straightforward and easy to navigate.This, imo, is one of the big reasons people so easily dismiss OOP. They put whatever data _they think they probably need_ in an object using setters/builders/what have you. This leads to abstractions of data that don't accurately reflect state. They will then let an external entity (service or whatever pattern) manipulate this data. At this point people might as well use something analogous to a C struct where anything can be done to the values. Objects are not managing their own invariants. When you rip this responsibility from an object, then nothing becomes responsible for the validity of an object. Due to this, people wonder why they get bugs and have trouble growing their software.
This also leads to things like "isValid". People don't understand that an object should be constructed with only valid data. The best example I've found of protecting variants and construction of valid objects to be this strategy in F#:
https://fsharpforfunandprofit.com/series/designing-with-type...
I'm yet to find a good way to do this in a language such as Java unfortunately.
I think OOP done well and applied in suitable scenarios results in entities that have internal locus of control; it's mutations of state are internally managed.
OOP done poorly has external locus of control where some external "service" or "util" or "helper" manages the mutation of state.
That said, Smalltalky hierarchies are a nightmare in most other languages because another key part of the Smalltalk env is the tooling -- staying inside a running system and being able to edit code from within the debugger is absolutely great and keeps you in a flow state much better than any other workflow I've been exposed to (including Lisp + Paredit + SLIME). The result is that editing class hierarchies in blub-y OO languages is usually a massive pain in the ass, while doing it in Pharo or a similar Smalltalk env is fun and painless. This is why you can't practically write Smalltalkly in Python/C#/Java/etc. even if on paper they have all or almost all of the same features.
What does this have to do with inheritance? This is just a generic function.
> And if you want to avoid huge if-else statements, you should put the code for the special cases in the classes, not in each function that operates on them
This doesn't sound any different from how people usually talk about inheritance.
I am not convinced it gives you anything more than composition does. Composition is very easy to setup and easy to change. And there are certainly functional ways to do runtime polymorphism.
It's a generic function with compile-time compatibility checks.
It looks like composition over inheritance has caught on as the better default in other languages, but in the CSS world people still cling to the cascade as a best practice for some reason.
The cascade is a wonderful idea that has unfortunately not played out well in practice. Anyone who thinks it's just a skill issue is deluding themselves. In all my years, I've never seen CSS that (a) leverages the cascade, and (b) scales elegantly. It all breaks down past a certain point.
Tailwind has it's flaws, but it doesn't have the scaling problem.
Personally, my dream would be if inline styles supported the full CSS gamut of pseudo selectors and the child element selector. Then you'd have the admitted benefits of not needing to synchronize the two files, along with the benefit of not needing to relearn all of CSS.
Edit: it's funny, in a way, that all these developers complaining about how CSS "doesn't scale" are likely writing their Tailwind in an large scale application styled entirely via CSS. https://github.com/search?q=repo%3Amicrosoft%2Fvscode++langu...
As a trivial example, let's imagine we need to apply a border radius to an element. Without Tailwind, it looks like this:
1. I find the element I need to style
2. I look at which class it's using
3. I navigate to my CSS file
4. I scroll down until I find the selector
5. I type "border-r", then tab on the auto-complete to fill in "border-radius"
6. I type the colon character, then space
7. I think about what unit is appropriate - rems, ems, pixels, percentage
8. I think about what value is needed for the design
9. I also look around to see if this style is used elsewhere
10. If the style is used elsewhere, I think about whether I need to refactor
11. I type in the desired value
12. I type a semicolon to mark the end of the statement
13. I type cmd+s to save the css file
Here's the same example, with Tailwind: 1. I find the element I need to style
2. I type `round` and wait for the autocomplete to present my options
3. I use my arrow key to select which one I want
4. I type enter to add the desired tailwind class
5. I type cmd+s to save the html file
The Tailwind interaction path takes less than half of the concrete steps to complete. But it's even more dramatic than that, because several of the steps taken in the first example require enough thought that it breaks your workflow and takes you out of your flow state. Then you have to get back into that flow state to keep working. But this keeps happening, so you're constantly stopping and restarting. With Tailwind, I tend to find myself staying in that flow state, because as I demonstrated above, there's very little getting in my way.Normal CSS is usually worse than this too e.g. you hit save, and your edit doesn't change anything, so you have to use the web inspector to hunt down which class is overriding your style then weigh up options for how you're going to refactor while jumping between multiple files. It's exhausting when you're trying to focus on styling.
That said, I agree that this doesn't work for pseudo selectors, very unfortunately, and I wish it would.
1. I find the element I need to style
2. I add/edit the inline style={{}} attribute I have for it by typing `sty<TAB>{borrad<TAB>`
3. I add a value
VS: 1. I find the element I need to style
2. I add/edit the className attribute
3. I pull up the tailwind documentation to find how to type the CSS I already have memorized in their DSL (I know some Tailwind by heart, but wayyyyy more CSS)
4. I wait for it to load
5. I scroll down to find the version of the class name that I need
6. I go back to the editor and add the class.
Also a very common flow for me is to edit the CSS directly in the browser, it's a much faster devloop than the fastest live reload server. In that case it's far easier to just copy from the `changes` view into the CSS than go through and remap everything from CSS into Tailwind.There's some movement towards ::part as a proposal to grant some mixin behaviors (https://developer.mozilla.org/en-US/docs/Web/CSS/::part) but I've never messed with it, and it's applicable only to shadow DOM. But mixins haven't been a huge issue for me in even enterprise-scale styling that I've done.
Opinion me, everyone has their own opinions on this, use whatever CSS style works for you. This is not me saying that BEM is the best for everyone, just giving a perspective that as someone who tends to stick to vanilla CSS and who generally kind of hates working with technologies like Tailwind or CSS-in-JS, BEM-style vanilla CSS made CSS pretty pleasant for me to work with; I have a lot more appreciation for the language now than I used to.
So if you're annoyed by CSS but also get annoyed by pre-processors or think that Tailwind is just inline CSS under a different name[0], you still don't need to be bound to the cascade -- potentially look into BEM. No technology or compilation or dependencies, it's literally just a naming convention and style guide.
----
[0]: yes, I have used it extensively, please don't comment that if I used it more something would magically click, I already understand the points in its favor that you're going to comment and I've already heard the style/framework suggestions you're going to offer. It's fine if you like Tailwind, it's great if it helps you write CSS, you don't need to convince me.
I think cascading is just a bad default, and I think methodologies like BEM agrees with this by teaching you ways to write CSS in ways that stops cascading from getting in the way.
Cascading styles are fine for styling how basic document content is shown (e.g. h2, p, a, li etc. tags) but outside of this, you generally don't want the styles of parent elements leaking into the styles of child elements. Cascading/inheritance styles is a useful tool to have, but not as the default.
I'm not saying Tailwind is perfect, but it's closer to "prefer composition over inheritance", where you can sprinkle in some cascading/inheritance where it makes sense.
You could throw the same criticism at Tailwind -- Tailwind can still expose you to cascade issues, not all Tailwind classes are single-level selectors under the hood and not all Tailwind classes only target one property. At the end of the day this is all compiling down to raw CSS, so in neither situation have you actually eliminated the cascade. But with both BEM and Tailwind you are much less likely to see those situations, and when they do arise they are less likely to introduce long-term maintenance problems and are more likely to be easy to address/encapsulate. If you run into cascade bugs with Tailwind, it's probably something you fix in like one file, instead of needing to search through five.
BEM doesn't technically interact with anything, it's just a style of writing CSS. There's literally no technology behind it, it is just a naming convention. But in practice, using a naming convention mitigates or eliminates a large number of cascade issues.
I'm again semi-inclined to agree, I just don't think I'd say it as forcefully; more that cascading styles tends to have a lot of downsides that people aren't familiar with and aren't taught.
My point isn't to badmouth Tailwind here; but debates about this sometimes boil down to "CSS purists" vs "Tailwind advocates" and my point is more -- nah, you don't have to like Tailwind to avoid the cascade. You can be a CSS purist and still avoid basic element selectors, your choice does not have to be either "do semantic styling targeting only semantic elements" or "jump on Tailwind and stick a bunch of styles inline."
I'm more sticking up for -- look, if you're someone who uses Tailwind, great, I don't have to tell you anything. You are already using a framework that (regardless of any other flaws it may or may not have) discourages you from using the cascade. But if you're someone who's in the position where you dislike CSS-in-JS or don't like using Tailwind, also great! I'm in that position too, I don't like Tailwind. But I still avoid cascade and basic element selectors and there are ways to basically eliminate most cascading styles from your codebase and eliminate most cascade-caused bugs even if you aren't going to use a pre-processor at all, and it's good to at least consider removing those cascading styles.
My only critique of Tailwind I would bring here is that sometimes I get the feeling that Tailwind advocates think that Tailwind invented this idea of component-based CSS, and it really didn't. But that's neither here nor there, and if someone is using Tailwind and it works for them, great. Life is way too short for me to argue with someone using a technology that they enjoy. Honestly, same with the cascade -- I think it can lead to long-term maintenance problems, but if you like it, fine.
However, if you're using CSS and hate it, and you also don't want to use Tailwind, then give BEM a try.
----
In general though, I would actually suggest that you can kind of use anything if you're not planning on forking the component library. Most of these 3rd-party libraries you're not going to be restyling, so long-term maintenance and scalability isn't really a concern, you're never touching that code.
And BEM is just a naming convention, so if you're pulling in a React component and it has a hook for you to attach your own classes, then attach a BEM-style class, otherwise pass in the styling information into the props the way that most JS components want. I've used BEM with React components, with Material design, with Angular components, etc... I don't know, I haven't really run into issues. I've even worked on a codebase that was a mixture of BEM CSS, 3rd-party CSS, and Tailwind. It was fine, I didn't notice any major issues. Typically 3rd-party component dependencies are not a major source of cascade bugs in my experience, but maybe I've just been lucky. Most components I've seen lock down their styles to the point where it's kind of a pain to even try to override them with CSS, and the specificity required to do so forces you to effectively isolate those overrides anyway.
Whatever component framework you're using will have its own customization API, and in my experience that usually won't be handled through CSS. If it is handled through CSS, it will probably be handled by attaching classes, and then when you attach those classes you can use BEM. The major annoyance in my experience is that (for me) BEM often works better than whatever customization system that the 3rd-party components is using and I get frustrated that I'm mixing more straightforward CSS that's easier to debug and design in-browser in with whatever property-based thing that the components expose. But I don't usually think I run into many bugs?
What BEM helps with is dealing with cascade, code organization, naming, and debugging/search. With a 3rd-party component framework, most of that stuff is out of your control, so just use whatever the framework wants and then use BEM for the stuff that is in your control.
If you have to do some kind of CSS-based override of a 3rd-party component that isn't being handled through a component-specific API, wrap it in a BEM-style class for your actual component so that it's a one-time customization:
.Input__Username .some_component_depencency input {
/* This is not ideal, but (imo) you'll still effectively never really see cascade bugs from doing it this way, and specificity will rarely be a concern. */
}
One thing that is nice about BEM is that because your own CSS is being scoped to specific named components, it actually becomes a bit easier to have a lot of 1st-party CSS living alongside 3rd-party CSS and know that your CSS is not going to break the 3rd-party CSS. So I tend to worry a lot less about what other parts of the code/dependencies are using when I'm using BEM, because I more confident while writing BEM that I'm not introducing bugs into the other CSS."Prefer composition over inheritance" was mentioned in an early page of the GoF book (the Design Patterns book) - in 1994.
I find this falls into generally two camps, those who want to fight the browser and those who don’t.
Those that tend to hate the cascade tend to fight the browser a lot, whether they realize it or not. Generally speaking, they want things to work a certain way (IE everything in isolation) and prefer to think of styles isolated bits.
The second camp tends to not fight the browser and embrace the browsers methodology. They embrace the cascade because it’s easier than fighting it, but it requires a more sophisticated approach and seeing styles in a wholistic manner, not isolated purely into components (though may be organized in a way that co-located them with relevant components)
Both work, ultimately, and modern tooling and approaches allow both to exist, but I will say, the second group often has a better grasp on keeping project maintainability over time in my experience.
So, wouldn't that be the browser fighting them, then?
They want something specific, and the broswer forces a paradigm upon them that they don't want.
They are the humans and the browser (well, the styling language of the browser) is the tool that should accomondate them, not the other way around.
It really depends on how you view the browser, IE should browsers be more adaptive to certain paradigms or should it set a reasonable paradigm and enforce it? I don’t know that there is a 100% right answer in this case, though they have moved to create better hooks for some forms of isolation (e.g. layers, scoping) but they fundamentally haven’t walked away from the cascade aspect.
It’s converging the two paradigms, for sure, but as I said, I don’t think either is wholly incorrect or correct.
Now if you wanted my opinion on the whole thing, I think the cascade is a fundamental element to be leveraged not avoided, but that’s me.
I don’t think CSS is the perfect tool for all browser-based styling but it’s the tool that’s there and it’ll probably work a lot better if you use it the way it’s intended to be used. If you want a screwdriver instead of a hammer… you have options (don’t target a browser, propose an alternative to CSS, use something that compiles down to CSS).
If someone wants to hammer nails and they're given a blender, then the blender might not be defective, but it surely is not the right tool for the job, and it's imposed upon them.
Few people ever loved CSS. The majority always either hated it or learned to tolerate it. Most who do CSS today use a few different paradigms on top to make them tolerable like BEM, or use different transpilers to get a better language, or directly control styling from code, with CSS-in-JS libraries or like React does it.
>I don’t think CSS is the perfect tool for all browser-based styling but it’s the tool that’s there
Sure, I never denied its existance. Just its design.
That's probably in the eye of the beholder and a necessity. If you are all in on the cascades then you have to limit the depth of your page structure, otherwise it becomes impossible to predict how things will ultimately render or what the impact of a change at layer 3 will have on the rest of the page.
Technically speaking, isolation is perfectly possible with webcomponents.
Another anti pattern I have seen is the over use of media queries to force the browser to do certain things rather than embracing relative sizing constraints via intrinsic design and letting the flexbox and grid algorithms do most of the heavy lifting.
Here though I want to point out isolation is relative, as is the cascade. I think it’s important to leverage the cascade wherever you can but that doesn’t mean you are leveraging it from top to bottom per say, but it does mean thinking more wholistic about the context of styling
Those efforts don't just dissappear quickly, since that was the only way available that worked across all browsers for nearly a decade.
Do these two camps align with your two camps? Those who want control fight the cascade, others just fiddle with it until it looks good?
I don't mean to disparage either camp, because we need both approaches in the real world.
When you are a beginner your S is close to zero, as you gain experience you develop increasingly better compression to be able to grow your effective S, but it’s never infinite. There’s always some size program where you will devolve to strategy 2, and all programs tend to grow in complexity until they reach the point where nobody understands them, like some kind of complexity Peter principle.
There was a company that I worked at 10ish years ago with a SaaS built around Drupal. They had one monolithic css file that was 25000 lines. It was a nightmare. When I took over the front-end rebuild, went through it line by tedious line and broke it out into discrete components.
I think about that from time to time when building logic or other functionality. I'd much rather have a self-contained bit of code than a big thing that is so intertwined I'm terrified to touch anything.
The cascade[1] determines how rules from multiple sources are merged.
[0]: https://developer.mozilla.org/en-US/docs/Web/CSS/Inheritance [1]: https://developer.mozilla.org/en-US/docs/Web/CSS/Cascade
The cascade is so much not like inheritance that I wonder if you meant either the concept of selectors in general, or inherited properties?
I meant the concept that when you e.g. apply the style "color: blue;" to an element, all child elements get the same style unless you override it. I'm not saying it's identical to inheritance, but it's similar in that changes and additions at the top-level ripple down to lower levels, which I find causes most of the problems when writing maintainable code.
This is the first thing you've said so far that I would push back against. Neither BEM nor Tailwind removes this behavior that I'm aware of. I thought when talking about the cascade you meant generic styles on elements like "p", "ul", etc... getting applied across separate components, or specificity of child selectors, or something similar.
If you really dislike styles being applied to children in the DOM that don't override those styles, I don't think there is a way around that other than web-based components and shadow DOM with isolated styles. Or I guess use a bunch of style resets beforehand I guess? Neither Tailwind nor BEM gets rid of child inheritance of applied styles; you can use @layer I guess, but that doesn't get rid of that behavior either, it just allows you a bit more control over style order.
If you're using Tailwind and you write:
<div class="text-red-400">
<p>Some text</p>
</div>
that text will be red. If you're using BEM and you write: <div class="Container">
<p>Some text</p>
</div>
.Container { color: red; }
same deal.Yes, so I mean if you add "color: blue" to "p", it's now going to start interacting with any element that's a child of "p" (which will probably be on all pages on your website so hard to predict and check what will happen).
BEM and Tailwind don't get rid of the behaviour of the color being applied to child elements, but it at least forces you to isolates these kinds of style changes to the component level (vs sitewide) which is what improves maintainability.
In my opinion, this was a failure of communication from the beginning, with a huge divide between "I want my website to look like this because I drew it on paper with my designer friend" and "css is a cool new way of doing decent programmatic ux/ui.
Variables and/or utility classes mostly solve this so not sure how it's related.
> Since then we've got more tools to control cascade: layers, scopes, inherit/initial/revert/revert-layer/unset for every property, etc. But people still insist on inline styling. I get it, it's much simpler. But they will keep being unhappy until they learn cascade and stop fighting tools. Yes, learning takes effort but so does resistance.
Similar to inheritance-like features in other languages, I have learned it and find composition is the better default. It's too complex for too little benefit so I avoid it wherever possible.
I find maintainable CSS approaches are all about reducing the blast radius of style inheritance. So people are writing maintainable in sprite of cascading, not because cascading helps.
> specificity is hard to grasp and order-dependent resolution
Specificity is another foot-gun to avoid for me. It's learnable, yet easy to forget later. When you're tempted to rely on specificity there's usually a more readable way to do it that's not going to bite you later.
Do you doubt the developers behind Tailwind library understand specificity and the cascade? They clearly must understand it, use it where it helps, and avoid it where it doesn't.
I agree. This sounds like saying cascading helps in simple cases but when it gets more complex it doesn't help.
I don't think anyone really has a big problem writing maintainable CSS for a document. It works fine there. Most CSS methodologies (like BEM) and frameworks (like Bootstrap, Tailwind) are there to help write maintainable CSS code for complex non-document web designs, where a big part of that is taming cascading.
I think a lot of pushback against approaches like Tailwind and JS frameworks are from people that are mostly styling documents. Complex landing pages and UIs are where the real headaches are.
I'm having trouble finding the original video, but Jacob Thorton (@fat) did an insightful talk on the whole affair called "Cascading Shit Show"
Trouble often comes down the line. We keep adding classes, and soon we find that the hierarchy no longer is “shaped like a tree”. A soccer ball is a spherical object but also a sports equipment and a bouncing object, and there’s no way to organize those into branches.
Our reality isn’t “tree-like”, except when we simplify it extensively.
Paradigm shifts happen and shake up even loosely organized into structures like human perception, and most people’s perceptions aren’t trees
Traits as a general concept would be very useful in many programming languages, but only some (like Scala) have proper support for it. In principle a Java interface with default methods (implemented methods) is also something like a trait, but very limited compared to traits in Scala. I have no statistics, but my impression is that inheritance is much less used in Scala, because the language has an easy to use alternative.
Another example is Go, where structs with their associated methods can be embedded, what is exactly composition supported by the language. Since Go doesn't support inheritance, programmers obviously needs to use this approach, so you would never see inheritance in Go programs.
So, my conclusion is that the usage of inheritance depends on what the language supports and how easy it is to use.
It's good for frameworks and extensible systems where your code needs to guarantee certain things will happen when running third party code.
An interface can specify or be part of a protocol as a special case, but to me the term doesn’t match the general case.
Of course it's not impossible for the preconditions for each operation of the interface to map exactly to a protocol so that the operations permitted in each state correspond 1:1 to the state transitions of a protocol. But that is not the general case, and not how people usually think about interfaces.
I don't know if you are familiar with the term interface description language (IDL), but generally IDLs are not suitable for specifying a protocol, in the sense of specifying (among other things) the protocol's state machine. It would be misleading to call them protocol description languages.
There are other ways in which "interface" and "protocol" differ. For example, we call HTTP a protocol, and things like SOAP and REST and CORBA. But we call a concrete REST API an interface, not a protocol, and we call what is defined by a WSDL an interface, not a protocol. A protocol can be used to access an interface (I use SOAP to access a monitoring API). An interface may support different protocols (I can operate different protocols through the Unix socket interface). We talk about Linux kernel interfaces, not about Linux kernel protocols (and if we would, we would mean something different by that). The two words are not interchangeable.
An interface describes the boundary between two things. A protocol describes a behavior between multiple entities. By the implementation of an interface we mean the implementation of one specific side, the one that exposes the interface, not the one that uses the interface. An interface can exist even if no one uses it (only one side exists). The implementation of a protocol, on the other hand, is split between both sides. You can't have only a one-sided protocol. If no one uses the protocol, neither side exists, so to speak.
void Handle<T>(T value) where T : IFeature1, IFeature2...[0] -- https://en.cppreference.com/w/cpp/language/constraints
I recall Pyright for Python is not able to find such call sites. But perhaps there are better implementations of the concept?
I could imagine a syntax that made you specify the protocol at the call site, e.g. for an object Foo that conforms to protocol Bar with method baz, you'd have to write obj->Bar->baz() as opposed to just obj->baz(). Or alternatively, Foo->Bar->baz(obj) if you want to make any given call site discoverable.
In your proposal by stating the name of interface you are calling you do get to see what is being called, and some protocol implementations such as the one in Python/MyPy you can name the protocol of the object you are invoking a method on.
But how about the implementation sites? I was under the impression with protocol based system you would implement a protocol by just having methods of certain names. So the set of functions that may be called would be the set of classes that happen to have a certain set of methods that conform to a protocol, which might or might not be made with the idea that they should be used in the protocol's context. You would need to rely on documentation, which isn't checked by the compiler.
Personally I think protocols are too ad-hoc for general program structure purposes, though they can have their uses. I consider both "class implements these abstract named interfaces" and traits (as in Rust) better.
Some APIs may expose themselves as requiring you extend a base class, and in that case you might as well do what the library author said. But overall? Yes, some things can work well with inheritance (eg. classical UI frameworks), but these are often equally good with a composition based API.
One job I was at I added a lint rule to flag `extends` as an error. You would need to add a comment to justify why you were using inheritance - it went over more favourably than I expected. I think I would suggest this for any new projects using a language that supports inheritance - discourage it with automation, but allow really good reasoning to prevail in review. It is the worse solution at least 99% of the time because it requires you predict a future that is painful to change.
How?
At least, that's how I think of it.
> Interfaces (via aspects, aspects, traits, whatever) tend to simplify, when the complexity is quantitatively extensive enough to outweigh the cost of the interface complexity creation + the complexity saved by using the interface.
That sentence is more complicated than anything that has been done with composition.
With inheritance, you can't know what something does until you know what everything it inherits does. The little method you override could change the entire execution flow of the code. It's extra steps with no benefits.
Making the relationship simple to create and hard to complicate, is why it endures, afaik.
Imagine that I have a class that has 15 methods -- 14 are a perfect fit as is, but 1 of them needs to be completely overwritten.
If I were to do inheritance, I would do `extends` on the class, and then `@Override` the offending method. Problem solved.
If I were to do composition, I could use the Delegation Pattern from the Gang of Four, but that would require me to write 14 do-nothing methods just to be able to override the one.
In general, composition has more use than inheritance. However, inheritance has a non-zero number of use cases where it is a better choice than composition.
It can be inelegant, clunky, and terrible practice if you're doing two totally different things in the method, but I've had a lot of cases recently where it was a nice simple solution that made sense in the context we used it.
For me, one time I like that we use simple OO is a 'DeletedMyClass' extending 'MyClass'- it does everything the same, just has a DeletedDateTime property. A few pages will do an 'is' check (we use C#) and render certain things differently, but apart from that, they both get passed around interchangeably. You could argue you could add an IsDeleted flag, but the default case is that most things we deal with are not deleted, and they shouldn't have to even know about that concept. So to me this is a very useful minimal case of OO.
And yes, excellent wording with the idea of "Should not even know about the concept". I think that is another good place where inheritance shines.
Say I have a well-designed, self-contained BusinessService class which is perfectly unit-testable.
Then someone comes along and sprinkles some @Magic bullshit on it. Now its true behaviour won't be exercised in plain junit - you need custom test runners with class-path scanning and late-binding configuration ("The gorilla holding the banana and the rest of the jungle" in Joe Armstrong speak).
If you want to reclaim your unit-testable class, you can divide it into two: SpringBusinessService extends BusinessService. Move all the Spring dependencies out of the latter into the former.
Now you get the best of both worlds - clean, unit-testable logic, and a service that Spring can obfuscate with middleware to produce 200-line-long stacktraces after a lengthy startup.
The worst example of inheritance I've ever found is in java Minecraft's mobs[0]. It is very deep in some places and has numerous examples of sub classes not fully implementing their parent interfaces.
Example 1: Donkey[1]
Donkey > Chested_Horse > Abstract Horse > Animal > Ageable Mob > Pathfinder Mob > Mob > Living Entity > Entity
Example 2: Blaze[2]
Blaze has a bit for 'Is on fire', but how is this any different from Mob 'Is aggressive'? Blaze > Monster > Pathfinder Mob > Mob > Living Entity > Entity
[0] https://wiki.vg/Entity_metadata#Entity
In Smalltalks as I know them, which is sporadically and shallowly, inheritance seems to be used as a factoring tool, when you break down functions into smaller functions there's a structure in which to place them with the implicit incentive to put them as high up as possible.
To me it seems to work pretty well, but it's also a rather weird environment where even classes themselves are objects.
In Java one can get around inheritance by using injection into an attribute in a wrapper class that extends or changes an object API. This leads to a similar decoupling as more functional composition, but you pay by having more objects swimming around in the VM during execution. Extending on the class level (i.e. in declarations of classes, interfaces, traits and the like) instead of the object level might have effects on performance and resource management.
Usually that likely doesn't matter, but it might. In Java-like languages I tend to use inheritance as a quick way to abstract over or extend functionality in a library class, I get the public methods 'for free' in my own class without having to write the wrappers myself. And that's usually where I stop, one level of inheritance, since Java isn't as inspectable and moldable as, say, Pharo, and doesn't give the same runtime ergonomics with large class/object trees.
the same wisdom can be applied elsewhere as well, there is no need to keep discovering the same truths in different contexts again, and again.
I am sure that hundreds of thousands of students have at some point heard that these hierarchical organization of data is how you should model a system.
It makes no sense to use inheritance in the business layer, because a single feature request can make a lot of the carefully crafted abstractions obsolete.
An argument against OOP where you first need to define/explain differences between composition/inheritance, Liskov Sub, compare and contrast with traits, etc is not really that effective when trying to mentor junior folks. If they understood or were interested in such things then they probably wouldn't need such guidance.
I think it’s almost always better to first think of data as a struct, with associated code that acts on that struct. As the old saying goes, show me your data structures and not your code, and you don’t need to show me your code.
That said, in my time with obj-c I never really encountered the problem of fragile base-classes - protocols and categories gave us just about everything we needed. (the foundation classes use implementation inheritance extensively, but we don't typically modify those too often :) )
Inheritance is like a lot specialised tools - it can be useful in some situations but I think those are rarer than supporters might thing. Completely refusing to use inheritance and always using inheritance both seem like extreme views that should be avoided.
I guess one could say this would be workable with proper tools, but the IDEs just aren't there. Move up a level in inheritance, ctrl-click on a call? It was overriden somewhere in the hierarchy, but the IDE will send you to the parent-class definition regardless.
More than once, I have found an inheritance hierarchy that made sense when first created no longer models the problem well. It becomes hard to change it at that point.
Frequently I find I really want to mix in cross cutting concerns across different hierarchies. This isn't really a surprise; most problems do not naturally decompose to only a single view of things.
I don't mind abstract base classes containing common functionality, so it's useful there, but mix ins would be better to be honest.
When inheritance is your only tool for interface, I absolutely agree that it looks fundamental, but the fundamentalness is coming from the interface side. Once you separate out interface from implementation, it turns out I have little to no use for the implementation portion.
I have also been programming in Go for a long time now. I sketched out an inheritance system once for an exercise. It's doable, though it's a terrible pain in the ass to use. I've kept it in my back pocket in case it is ever the right solution to a problem even so... but it never has been. And that's not because of any irrational hate for the pattern itself. I've imported other foreign patterns sometimes; I've got a few sum types, even though Go isn't very good at it, because it was still the best solution, and I've got a couple of bits of code that are basically dynamically typed, again because it was the best solution, so I'm not against importing a foreign paradigm if it is the correct local solution. Inheritance just... isn't that useful. The other tools that are available are sufficient, and not just begrudgingly sufficient or "I'm insisting they're sufficient to win the argument even though I know they really aren't"... they really are sufficient. Functions are powerful things, as the functional programming community (it's right there in the name) well knows.
If you have to model your data exactly once I believe it can work. But if you are doing any kind of agile, or even somewhat flexible waterfall, you will find out that at some point none of your data models work.
I think the point of the article is that in some (very popular) languages, inheritance comes with a lot of baggage - precisely because classes in those languages support 3 different use cases. It's not saying that your usage is bad, but that your language didn't provide you with the best tool(s).
A lot of how people use classes can be solved via ADTs - which are much simpler to grok.
A lot of how people use classes can be solved via namespaces/modules, but not all languages have good support for them - so they use classes.
Etc.
The immense pain and suffering that is related to Java and its ecosystem is because of the terrible organizational conditions that tend to co-occur with usage of Java. Big slow companies, long boring meetings, arguments about design patterns, "architects" who haven't written code in ten years, etc etc. Java correlates with, but does not cause, those conditions.
Another way of saying it: if Java was good enough for Nutch to use to write Minecraft, it's good enough for the vast majority of business purposes.
> There are two types of programming languages: The ones people complain about and the ones nobody uses.
I’ve seen more than one lifetime’s worth of nightmare code written in Java. I agree with the GP commenter - I don’t think the problem is Java (the language) so much as the culture surrounding it. The Java programmers I’ve worked with (all smart people) seem addicted to solving all problems my writing more Java. More interfaces (most of which just have a single implementor). More classes. More files. More abstractions. You pay a massive tax for that any time you edit your software - since inevitably you're going to end up unpicking hundreds of lines of code that could have been two if statements.
I’ve never seen this abstraction vomit disease be quite this bad in software written in other languages. You can find a bit of it in C#, C++, go and Python. But not as bad as Java. In typescript and rust, most of the code I work with focuses a lot more on directly solving the actual problem at hand.
I don’t think the problem is the language itself. Java is a fine language. But for some reason, it’s become a magnet for a certain kind of developer that I frankly never want to work with. Developers who would never use 5 lines of code when 100 would do. Developers who abstract first and understand the problem they’re solving later. It’s disgusting.
That’s just your personal experience with these languages. I would even go as far to say that there is some survivorship bias — certain projects might have survived in java that collapsed under its own weight in some other platform, making the “complex nightmare projects” overrepresented in java, even though it is due to a positive thing.
But there’s plenty of large, apparently healthy, projects written in other languages. The Linux kernel (C). Chrome (C++). Unreal engine (C++). Postgres. And so on.
My claim (and criticism of the culture around java) is that I’ve seen a lot of large Java projects at medium to large companies which didn’t need to be large projects at all. And wouldn’t be if they were built in a different style. The claim that building verbosely is good is almost never justified with data, and always seems deeply suspect to me. My instincts often say “I could rewrite this monolith with 2 smart developers in 3 weeks”. After 30+ years writing software, I’ve learned to trust my instincts.
Happens with a lot of language communities. C#, good grief, not much better than Java. Go(lang) community's favorite two words are "idiomatic Go". You can't read any post without seeing those two words. Go has like what, seven keywords. Let it go, man.
I totally agree. I have worked on applications that would have collapsed under their own weight if it wasn't for Java. The strong type system, stable runtime and standard library APIs, as well as great tooling allows code to live far longer than it would in some other language ecosystems.
However, don't make the mistake of dismissing all of the criticism. The truth is often nuanced, and OOP/Java does have some serious downsides and footguns. There is room for both criticism and praise.
Ultimately, great systems aren't created by throwing a language and problems at a collection of random people. Great systems happen when good decisions are consistently made over time, and those are context dependent.
Bertrand Meyer’s open-closed principle can be read that way: The superclass is closed to modifications. But that goes against the principle of decoupling interface from implementation: The superclass interface is now stuck with the existing superclass implementation.
At one point "Object Oriented" became "Blockchain" (or now Gen AI) of those times. You had to be "object oriented" in order to be taken seriously. This applied to everything. Even finished software products were called "built using object oriented".
The esoteric concept of inheritance became popular after that. At some point it became so popular that people figured out that it is not really a good thing.
Causing engineers to completely change their mental model of how the code runs, which I still have intuitive trouble imagining correctly and I see it with other developers as well. A lot of energy goes into trying to understand how to do a simple action. It is much harder to read the code and have a correct mental model as well.
I feel like I spend more time and energy on how to get something done functionally rather than focusing on what the correct business logic should be.
not being familiar with fp doesn't mean it's objectively worse
Because at some point mainstream OO languages made inheritance easy and composition harder, nothing more.
Composition with Java used to be verbose. Inheritance declaration was simply a single keyword.
If Java had "mixins" from the start, people would have used composition way more.
FP in the industry is a niche thing, so the two phenomena are not really comparable.
The trick though is you actually have to use modern Java, which means you need to both be on the right version of Java, and have developers that understand the value/power of these newer constructs. Which is surprisingly rare for a programmer that self-identifiers as a Java programmer.
if you have a person class and you have students and teachers
the correct thing to to is NOT to make student and teacher inherit from person
it's to make student and teacher have a person attribute + the other attributes that constitute a student and teacher respectively
Any big enough system will have parts that are most sensibly built object-oriented, and other parts that are more reasonably functional, plus anything else you can think of.
The big problem these deep inheritance hierarchies cause is mediocre programmers often think they're writing good code (often high focus on maximal reuse) but the interfaces and clarity are terrible.
How about arguments for OOP? Especially for things which can't be done better elsewhere.
The hallmark of it is that "objects" are passed as first arguments to functions in languages that don't support syntactic sugar for methods.
The zoo animal metaphor that you often see used in teaching inheritance (lions are animals, simba is a lion all animals have a length, but lions also have claws etc.) is a bad perspective and you should never use inheritance to model a problem like that in the real world.
The only time I’ve used inheritance, has been on toy problems that didn’t need it. Not to say it doesn’t have its place but when I encounter it, it’s time consuming to peel back all the layers or deal with idioms that don’t quite fit a problem that a developer has enforced through inheritance.
See https://news.ycombinator.com/item?id=39999644
It's very useful when a base class provides an incomplete implementation, and the child class completes the implementation.
Sometimes I use it when testing, where the child class alters some behavior in a way that mocking can't do correctly.
In C# / Java, inheritance is used for setting up filters for exceptions
The problems with inheritance are:
- people shove code in the base class to dedup it without thinking about design
- people add public and protected methods without thinking about interface design
- the names of the base class and the public and protected interfaces are exactly the same thing
The last point is that if you have a FooBase class which is public then you can wind up with a bunch of List<FooBase> containers that are coupled to the base class and external methods that take FooBase parameters along with a bunch of concrete instances which are dependent upon the FooBase class protected interface and code implementation. This creates the brittle base class problem.
If instead you had an IFoo interface and only ever used List<IFoo> instead of FooBase anywhere then you could always define FooBasev2 which implemented IFoo as well and FooBase and FooBasev2 can coexist in your codebase without having to break any external consumers (open-closed principle in practice).
If base classes were only allowed to be used in inheritance and couldn't be parameters, generics, etc then that would force users to create base classes and public interfaces in pairs and would decouple their names and by writing the public interface down as its own thing developers would be more likely to focus on that design.
And really inheritance is just syntax sugar around having a component (the base class) which has automatic delegation of of the public interface and protected interface to it in the inheriting object, with so little typing that it becomes easy to not think about what you're doing -- and while reusing the same name for three different things and creating tight coupling, and you can't dependency inject different baseclasses at runtime. If languages had better terse syntax for delegation then composition would get a lot easier to use like inheritance is (which is something that Go oddly enough gets more-or-less correct, dunno why they didn't mandate piles of boilerplate for delegation like they did for error checking).
I feel like a lot of people blow this up like it's so e huge problem when in reality this is a pretty trivial refactor.
If all you do is consume other people's library/frameworks then yeah, nothing really matters, do whatever you want, refactor at will.
It seems to me that a lot of people obsess a bit too much about this stuff in places where it doesn't matter.
Which, if its a common thing, suggests that there should be syntax for like:
interface IFoo from FooBase;
That makes an interface from the public members of a class.Or, perhaps even better, type systems should enablr.a distinction between:
class ConcreteFoo inherits FooBase; // normal implementation inheritance class ConcreteFoo implements FooBase; // is required to (compiler checked) present the public interface of FooBase, and is type-compatible, but doesn't inherit implementation
So concrete classes can also serve as interfaces, and purely abstract classes are identical to interfaces.
I agree and like this principle, however, but what would be the difference between the base class and a mixin class then?
In other words, in the inheritance tree, the class providing the common implementation must sit between the interface class and the concrete classes.
I am sure that many, many people have had the experience of a professor tell them a nice sounding story about purely hierarchical data and went on thinking that this is the way data should be modeled.
Thankfully I believe that nowadays many people have understood what works and what doesn't work in OOP and we can actually try to get away from that model of data.
In the late 90s, 2000s and maybe even now, people see inheritance and think that it makes sense. They make the vehicle, the car, the sedan and then get stuck, when all they really needed was an x and y point and a velocity.
click click click click click click click click click click click click click click click click click click click click click bait
Most specifically, often when encountering a situation in which I have two slightly different classes/types that need to be polymorphic with each other, all the standard non-inheritance based approaches seem to require a lot of outright identical code implementing a shared interface, a bunch of boilerplate composition proxy methods that just point to methods in a composed class, or weird and obscure language features that never really feel like the "intended" approach.
Typescript especially seems to demand really awkward and obtuse implementations of basic mixins or abstract classes and the like, I always feel like I'm missing the more "intended" approach whenever I try to share code between similar classes.
Often discussions around this are awash with talk of traits and delegates and other cool features that seem to only exist in a handful of languages, and never the ones I happen to be required to use at that given moment.
One advantage of this is that it exposes that 'inheriting' a method is really just applying the same base function to all classes that support a particular interface. You can override a method by specialising the function for a subinterface. This interface can just be a marker that says 'this struct has these fields, and semantically is a 'IMyInterface').
Of course object oriented programming languages tend to encourage private and protected properties and such, which force all of this to take place inside the class. At first I though that that was the best way to avoid a mess, but it prevents you from doing this, possibly leading to more code duplication. And after some more experience with python there's something to be said for python's approach of just using name conventions to point out when to be careful.
function makeCounter(initial = 0) {
let count = initial;
function get() { return count }
function inc() { count += 1 }
function dec() { count -= 1 }
return {get, inc, dec}
}
// works just like a normal object
const ctr1 = makeCounter();
ctr1.inc()
// but this works too. try doing this with a class instance.
const {get, inc} = makeCounter()
inc()
This is the typical style of most VueJS code (though it would replace count with a ref and probably call the function `useCounter`). TypeScript is smart enough that it can infer interfaces and even classes from such literals should you go that way, full support for substitutability checking and all. class Foo(private val myMap: Map<String,String>=mapOf()): Map<String,String> by myMap
This creates a Foo class that is a Map that delegates to the myMap property under the hood. So you side step a lot of the issues with inheritance; like exposing the internal state of myMap. But you can still override and extend the behavior. Replacing inheritance with delegation is built into the language.The net result of this is that inheritance is rarely needed/used in Kotlin. It's there if you really need it but usually there are better ways.
Scala and other languages of course have similar/more powerful constructs. But Kotlin is a nice upgrade over Java's everything is non final by default (or generally defaulting to the wrong thing in almost any situation). Go has a nice pattern based on duck typing where instead of declaring interfaces, you can just treat something that implements all the right functions as if it implements a type. Rust similarly has some nice mechanisms here. All these are post-Java languages that de-emphasize inheritance.
I've lost count of how many talks I've watched by Kate Gregory where she advocates for people tagging everything they can as const in their C++, but asking people to eat their veggies never works.
As a graphics programmer for many years I can also say that OOP is not really a good fit design wise for modern rendering engines, it mostly just gets in the way.
If it's based on components, wouldn't that mean you would think of it as being... _composed_? I typically don't hear "it's made of many components" and think of inheritance
Examples are usually something like having a base entity, a player that inherits from entity and an enemy that inherits from entity. Then you have a magic user and a barbarian inherit from enemy but now you also want your player to be able to use magic. Traditional OOP doesn't make it easy to share the implementation.
Even with composition and interfaces you still have problems with most traditional OOP languages when you want to do things like change the set of components of an entity dynamically at runtime (player gains or loses the ability to use magic during the game).
"Is a" is often not the relationship you want to model. A player and an NPC both have the "has a" relationship to an inventory, not the "is a" for example.
I've been doing a lot with LeafletJS lately, and it has some light inheritance around its drawing primitives (things like boxes, circles and lines you draw on top of maps). It works well.
For 3D engines you still need to be quite careful with where you apply it though. It's possible to get into a huge mess if you use it too much.
Is there a place outside of GUI programming where inheritance is used in non-habitual and useful way? I can't think of many.
More often than not you have a final class that you are supposed to use and the petrified hierarchy above that is of not much use.
After a while you get to closures, which are pretty much objects without classes, and then factory functions that produce closures and there you have something like a class as well. If I were to design a course I'd probably follow this flow to get to 'OOP' in that sense.
I prefer to say that we naturally classify objects around us using fuzzy boundaries like "a house", "a cat", which don't exactly mean anything: they are templates we use later to actually generate an actual house, or an actual cat (on a piece of paper as a drawing for instance).
The world being separated between our internal classes and the interactive instances of them, we can code that way too: we look for what we actually need in a concept, name it and define only what we need, then instantiate several concrete actors interacting together. Each actor is complete, well defined, with contracts for interactions.
You can then start building on top of this more complex systems, talking in English and defining rationally the world of your little model.
If you start talking about functions, you're basically aliasing a memory address for a bunch of code: you'll goto that function address, with some parameters at some other address, do a bunch of things and put the result in another address for the next function to process. You're building at best a suite of pipelines, which ends up being a little bit too technical and static for my taste.
Another way to defend modelling a software with classes and object, in my view, without trying to talk about functions, is the blank page effect: imagine arriving at random at some assignment. You understand vaguely the business problem, now you need to code a solution. You have 6 months, will generate a few millions a year and will require a team of 10 people eventually to test, maintain, operate and debug: you start with defining functions generating functions with callback parameters in Python, or you launch IntelliJ and you do a Java class model ? I'd be terrified modelling complex problems with just functional pipelines, I think it's just way more obvious to talk about objects at all time if you're gonna do something that is not just processing inputs into well defined outputs.
'Here is a way to do simple math, here is a way to glue text to text, now that has grown a bit, you can shorten it for repetitions by giving it a name like this', and so on.
And to me, starting out functionally is easy. Data is pretty much always a given, it's very rare in practice that I first need to model speculatively and then generate data ex nihilo unless I'm creating mocks. Usually there is a data source, a file, a network API, whatever, and then I start building against that. Small, simple things at first, like mapping all the input to an output interface, then add transformations and output for these, and so on.
In general I spend more time thinking about the shape of data I already have than architecting code or inventing models.
Inheritance isn't necessarily bad.
If anything is common, someone will try to seem like a brilliant iconoclast by writing an essay against it. The particular essay in this case was from a Pythonista (not surprising, given Python's weird love/hate with objects):
https://solovyov.net/blog/2020/inheritance/
And not everyone uses inheritance.
Maybe you have another question, like "why would someone use inheritance?"
In python you've got polymorphism, mixins etc, so the reason for this suggestion doesn't really apply.
Django heavily uses inheritance for the controllers for example - and it works very well.
If you're composing a class out of multiple chunks of functionality then all of the bits you want to call need to be public. If you're sublcassing and overriding then the bits you're overriding can be internal, and not part of your public API.
C - You don't use inheritance because it literal has none, but you can handroll dynamic dispatch (interface style Abstract Base Classes)
Rust - Also doesn't have it and people seem perfectly happy with generics, sum-types, and `dyn Trait`
I believe it doesn't make any sense, you can use different patterns to have the benefits without creating a complicated type hierarchy where state and functions are mixed across different files.
Whenever I would use inheritance I use the builder pattern.
But in work code I don’t see it much. If it is used it is light weight. Often used incorrectly for splitting code into different files rather than actual inheritance (giveaway is one one derived class!)
The core reason everyone uses it seems to me to be essentially the same reason the former groups hate it - it’s what everyone else does, it’s an incredibly square and “day-job”-esque approach to one’s tooling and architecture, it often reeks of bureaucracy, compartmentalization, and organizational superstructures often unrelated to the technical work, dictated primarily by managerial fiefdoms and needs to organizationally coordinate, and it’s a well-defined space with extremely predictable solutions and headaches, and therefore has very little excitement or novelty to offer.
The only problem is if you're bad at using the hammer, and you only know how to use a hammer.
Where they differ from OO languages such as Java and Python is of course that they avoid traditional inheritance, offering more restricted inheritance-adjacent features (typeclasses/traits/interfaces and struct embedding).
Like many others I find Go disappointing in many regards. (Although Go's channels, goroutines, select, and implicit interfaces are beautiful). One disappointment is the way that methods are not indented or nested within the struct definition/interface that they are defined on. This is, IMO, a silly fig-leaf that is attempting to make the language look "not OO" in a superficial manner, but all it ends up doing is making it hard to see where your implementation of one interface ends and another starts.
First up, we've got the crowd who treats CSS like it’s the ugly stepchild of inheritance, preaching the gospel of "composition over inheritance" as if it's the latest fashion trend.
Then there's the old guard, clutching their pearls and insisting that inheritance isn't the problem—it's just misunderstood, like a moody teenager.
Cue the functional programming aficionados, sashaying in with their "functions over classes" mantra, ready to throw shade at OOP's entire existence.
And let’s not forget the star of the show, the claim that inheritance is the VIP at the GUI and game development party, although some party poopers argue that the cool kids moved on to ECS systems ages ago.
Meanwhile, the language innovators are flaunting their Kotlin and Swift ensembles, dripping in modern features that promise to make inheritance feel so last season.
In the midst of this fashion war, there's a heated debate on whether we should be dressing our newbie programmers in OOP gowns or functional frocks from day one. And, honey, let’s not even get started on the industry’s trend chasers, who once hailed OOP as the next big thing, only to ghost it faster than you can say "blockchain."
In the end, it’s clear that in the world of programming, inheritance is either a timeless classic or a faux pas waiting to happen, depending on who you ask.
Definitely one of the better ones though, very impressive. AI really is great at making some very impressive things. But since AI doesn't "get it", a lot of what it produces just feels off or misses the point.
Then, madness...
Let's stay I have a base class A. Let's say I have 70 classes that inherit from A.
These classes must serialize/deserialize from disk.
My choice here is that I can add a new data member to A and then update the read/write method in one place, or I add a new data member to all the classes that are derived from A, meaning that I get to update 70 classes. Seriously? Who would think this is a good idea? You'll have to write 70 times as much code... there will be times when that's definitely a very bad idea.
Maybe don't use inheritance. Then give every class its own "Name" data member like std::string c_nName. Every class can have its own function to set the name, or we could use an unencapsulated free function and do things like C. Except then the experts will say things like "well, now you're doing C with classes". Then you get a ticket that the user can enter bad names. You can then figure out some way to validate the names, right? That could be a free function, or maybe write some class called NameValidator. Except now the experts say you're writing C++ like Java. Too many classes, too few classes, too many objects, too few objects, too may free functions, too few free functions.
C++ expert of the month may say X, Y, Z, but look at Microsoft's APIs.
Inheritance is everywhere.
Look at any open source C++ project.
Inheritance is everywhere.
Does that mean that you should make something like:
A
--B : public A
----C : public B
------D : public C
--------E : public D
----------F : public E
Or this:
A B
\ /
C
Not unless you have a damn good reason. But the point is that inheritance is a valid tool and it can reduce bugs, code duplication, maintenance.Right tool for the job, yeah? I once read something in the C++ FAQ that said something like "Don't take advice from people who don't understand your business problems."
The alternative case is that you have 70 classes that CONTAIN A. You still only have to change A.
a.Read( ... )
a.Write( ... )
And depending on the use case and design, they also need methods such as: A& GetA() { return a; );
const A& GetA() const { return a; }When I then finished the book and its examples, I wanted to do my own exercises and went through about 1-2 very painful years trying to model my things using Inheritance and totally blamed myself for being too stupid to do software development.
Only then I started searching and finding essays and blog posts of people criticizing OOP and esp. Inheritance for exactly the things I was struggling with. This felt like a relief!
My question: Why is Inheritance still taught as a silver bullet? I'm seeing university courses using Inheritance both for Domain and infrastructural code reuse at the same time. Esp. in Germany I see a lot of stupid 90s alike "programming" courses and, no shit, according code bases. It's as if they were living in a gigantic bubble.
inheritance is the worst as you grip with you types.
just use strategy patterns instead.
Sometimes inheritance is extremely useful. Sometimes it's harmful. Sometimes it's not the best but the most practical nontheless.
Language concepts are tools. These people with extreme black and white views live under the delusion that there can be some "Perfect Language which will cause the program to never develove/get technical debt TM".
Pure delusion.
Real life is an approximation where you do what is best in practice, not in some delusional daydream of perfection which breaks the moment you try and bring it to reality.
We have a million flavors of the month but in the end it doesn't really matter. Pick a flavor and there will be teams that are successful and teams that are not. No silver bullet will make the non-successful teams magically turn into successful ones.
"I/We failed with <insert language feature>" never generalizes to "<insert language feature>" is bad.
Then the community constantly sits there and acts like a bunch of geniuses because some programming pattern has been discovered.. except it's 50 years old.
In this case, published in a book 30 years ago, at least:
I thought the same for decades, then I met Groovy and Grails. Of course, it’s not bad in an absolute sense (IMHO that doesn’t make any sense), but when some problem can be found only at runtime, when any proper compiler would catch that compile time, it’s hard to argue that it’s a good direction. Especially when TypeScript made it quite obvious what’s possible only with simple type checking.
Typescript's type system is anything but simple.
“If C is so unsafe and C++ is so unwieldy why does anyone use them?”
Because that’s what they learned, or because that's how the codebase is structured when you get there. When all you have is a bike you’re going to ride that shit everywhere.
You rarely if ever get the chance to start from scratch at work (where most code is written) and your job is to follow the pre-set pattern most of the time unless it becomes completely untenable. Sure, inheritance is worse in pretty much every way, but if everyone you're hiring does it that way, your codebase is built that way, etc, you're going to make the best of it because that's your job.
I gave Rust a try, and lots of nice things about it. But many of its libraries are not very mature.