6 ms·
> The bottom line is, no one ever really used inheritance that much anyway If you think that, you have no idea how much horrible code is out there. Especially
by josephg 1y ago
> The bottom line is, no one ever really used inheritance that much anyway
If you think that, you have no idea how much horrible code is out there. Especially in enterprise land, where deadlines are set by people who get paid by the hour. I once worked on a java project which had a method - call a method - call a method - call a method and so on. Usually, the calls were via some abstract interface with a single implementor, making it hard to figure out what was even being executed. But if you kept at it, there were 19 layers before the chain of methods did anything other than call the next one. There was a separate parallel path of methods that also went 19 layers deep for cleaning up. But if you follow it all the way down, it turns out the final method was empty. 19 methods + adjacent interface methods all for a no-op.
> The bottom line is, most people don't have any original opinions at all and are just going with whatever seems to be popular.
Most people go with the crowd. But there's a reason the crowd is moving against inheritance. The reason is that inheritance is almost always a bad idea in practice. And more and more smart people talking about it are slowly moving sentiment.
Bit by bit, we're finally starting to win the fight against people who think pointless abstraction will make their software better. Thank goodness - I've been shouting this stuff from the rooftops for 15+ years at this point.
- HdS84 1y agoI don't think Inheritance is always bad - sometimes it's a useful tool. But it was definitely overused and composition, interfaces work much better for most problems. Inheritance really shines when you want to encapsulate behaviour behind a common interface and also provide a standard implementation. I.e: I once wrote a RN app which talked to ~10 vacuum robots. All of these robots behaved mostly the same, but each was different in a unique way. E.g. 9 robots returned to station when the command "STOP" was send, one would just stop in place. Or some robots would rotate 90 degrees when a "LEFT" command was send, others only 30 degrees. We wrote a base class which exposed all needed commands and each robot had an inherited class which overwrote the parts which needed adjustment (e.g. sending left three times so it's also 90 degrees or send "MOVE TO STATION" instead of "STOP").
- josephg 1y ago> I don't think Inheritance is always bad - sometimes it's a useful tool. I can only think of one or two instances where I've really been convinced that inheritance is the right tool. The only one that springs to mind is a View hierarchy in UI libraries. But even then, I notice React (& friends) have all moved away from this approach. Modern web development usually makes components be functions. (And yes, javascript supports many kinds of inheritance. Early versions of react even used them for components. But it proved to be a worse approach.) I've been writing a lot of rust lately. Rust doesn't support inheritance, but it wouldn't be needed in your example. In rust, you'd implement that by having a trait with functions (+default behaviour). Then have each robot type implement the trait. Eg: trait Robot { fn stop(&mut self) { /* default behaviour */ } } struct BenderRobot; impl Robot for BenderRobot { // If this is missing, we default to Robot::stop above. fn stop(&mut self) { /* custom behaviour */ } }
- esailija 1y agoThe problem is of course that there is no useful default behavior you can define when the trait is so isolated and generic.
- jjmarr 1y agoDon't mix implementation and interface inheritance.
- josephg 1y agoIt doesn't have to be "so isolated". The trait can still have required methods that don't have a default implementation. Eg: trait Robot { fn send_command(&mut self, command: Command); fn stop(&mut self) { self.send_command(Command.STOP); } } struct BenderRobot; impl Robot for BenderRobot { // Required. fn send_command(&mut self, command: Command) { todo!(); } } This is starting to look a lot like C++ class inheritance. Especially because traits can also inherit from one another. However, there are two important differences: First, traits don't define any fields. And second, BenderRobot is free to implement lots of other traits if it wants, too. If you want a real world example of this, take a look at std::io::Write[1]. The write trait requires implementors to define 2 methods (write(data) and flush()). It then has default implementations of a bunch more methods, using write and flush. For example, write_all(). Implementers can use the default implementations, or override them as needed. Docs: https://doc.rust-lang.org/std/io/trait.Write.html https://doc.rust-lang.org/std/io/trait.Write.html Source: https://doc.rust-lang.org/src/std/io/mod.rs.html#1596-1935 https://doc.rust-lang.org/src/std/io/mod.rs.html#1596-1935
- silisili 1y agoI think that's part of the charm of Go, as a language/community. I've worked with countless people who came from Java, who try to create the same abstractions and factories and layers. When I chide them, it's like realizing the shackles are off, and they have fun again with the basics. It leads to much more readable, simple code. This isn't to say Java is bad and Go is good, they're just languages. It's just how they're typically (ab)used in enterprises.
- josephg 1y ago> This isn't to say Java is bad and Go is good, they're just languages. It's just how they're typically (ab)used in enterprises. Yeah; I agree with this. I think this is both the best and worst aspect of Go: Go is a language designed to force everyone's code to look like vaguely the same, from beginners to experts. Its a tool to force even mediocre teams to program in an inoffensive, bland way that will be readable by anyone.
- lelanthran 1y ago> But there's a reason the crowd is moving against inheritance. I doubt it; the majority of code is in enterprise projects, and they do Java and C# in the idiomatic way, with inheritance. I'm working on an Android project right now, and inheritance is everywhere! So, sure, if you ignore all mobile development, and ignore almost all enterprise software, and almost all internal line-of-business software, and restrict yourself to what various "influencers" say, then sure THAT crowd is moving away from inheritance.
- lmm 1y agoJava and C# are already a huge step up from what came before, since they at least introduce the concept of an interface as a distinct thing from a parent class. The fact that you don't notice that is proof that progress does happen, if only slowly.
- pasc1878 1y agoJava wasn't the first to do that Objective-C (10? years before) had interfaces. Even C++ has that with multiple inheritance - some parents can just be interfaces. As to whether Smalltalk needs interfaces see https://stackoverflow.com/a/7979852/151019 https://stackoverflow.com/a/7979852/151019 and https://www.jot.fm/issues/issue_2002_05/article1/ https://www.jot.fm/issues/issue_2002_05/article1/
- lmm 1y agoObjective-C and Smalltalk were always niche languages, at least by comparison to Java and C#, and I think Smalltalk fans underestimate the value of many things. C++ does not (or at least did not at the time) have a concept of interfaces. There was a pattern in some development communities for defining interfaces by writing classes that followed particular rules, but no first-class support for them in the language.
- kgeist 1y ago>no first-class support for them in the language. An interface is just a base class none of whose virtual functions have implementations. C++ has first class support for it. The only thing C++ lacks is the "interface" keyword.
- eloisius 1y agoAs they say about OOP, everything is somewhere else. The only part of inheritance I’ve ever found useful is allowing objects to conform to a certain interface so that they can fulfill a role needed by a generic function. I’ve always preferred the protocol approach or Rust’s traits for that over classicist inheritance though.
- eru 1y agoAnd Rust's traits can sort-of inherit from each other.
- josephg 1y agoI'm fine with trait inheritance. (If you want to call it that - its maybe better to describe it as trait preconditions.) I'm fine with it because trait inheritance doesn't increase code complexity in the same way C++ / Java class inheritance does. If you call foo.bar(), its usually pretty obvious which function is being called. And you only ever have to look in one place to see all the fields of a struct. In C++, its common to have a class method say "blah = 5;" or something. Then you need to spend 5 minutes figuring out where "blah" is even defined in the class hierarchy. By the time you find it, you have 8 code windows open and you've forgotten what you were even trying to do. And thats to say nothing of all the weird and wonderful bits of code which might modify that field when you aren't looking. Ugh.
- eru 1y ago> And thats to say nothing of all the weird and wonderful bits of code which might modify that field when you aren't looking. Ugh. Agreed, mutation tends to make everything worse and definitely more complicated. Mutation is a powerful technique, but needs to be treated with care. Haskell and Rust (and Erlang) amongst others have some interesting approaches for how to recognise the danger of mutations, but still harness their upsides. Haskell even has quite a few different approaches to choose from, or to mix-and-match.
- osigurdson 1y agoThat is probably because you identify with the Rust tribe. Anything that Rust has is good while things in other languages have seem less good. This is fine, use the innate tribe affinity energy to get better at Rust.
- wolvesechoes 1y ago"But there's a reason the crowd is moving against inheritance" Yes, in our fad-chasing industry the pendulum has moved in the other direction. Let's wait few years. There is nothing wrong with OOP, inheritance, FP, procedural, declarative or whatever. What is bad is religious dogma overtaking engineering work.
- ab5tract 1y agoI’m surprised this is considered a controversial take. You can write spaghetti in any language or paradigm. People will go overboard on DRY while ignoring that inheritance is more or less just a mechanism for achieving DRY for methods and fields. FP wizards can easily turn your codebase into a complex organism that is just as “impenetrable” as OOP. But as you say, fads are fads are fads, and OOP was the previous fad so it behooves anyone who wants to look “up to date” to be performative about how they know better. Personally I think it’s obvious that anyone passing around structs that contain data and functions that act on that data is the same concept as passing around objects. I expect you can even base a trait off of another trait in Rust. But don’t dare call it what it actually is, because this industry really is as petulant as you describe.
- josephg 1y agoI have a slightly different take: I think every new technology or idea is created because it solves some problems, but in the long run, we'll discover that it creates other problems. For example, transpiling javascript, C++ OO, actors, coroutines, docker, microkernels, and so on. When a new idea appears, we're much more aware of the benefits it brings. But we don't know the flaws yet. So we naively hope there are no flaws - and the new idea is universally good. But its rare to change something and not have that cause problems. It just always takes awhile for the problems to really show up and spoil the party. I guess you could think of it as the hype cycle - first hype, then disillusionment, then steady state. Sometimes I play this game with new technology. "In 10 years, people will be complaining about this on hackernews. What do I guess they'll be saying about it?". For rails, people complain about its deep magic. For rust, I think it'll be how hard it is to learn. For docker, that it increases the size of deployments for no reason. And that its basically static linking with more steps. Calling everything a fad is too cynical for me, because it implies that progress is impossible. But plenty of tools have made my life as a software developer better. I prefer typescript over javascript. I prefer cargo over makefile / autotools / cmake / crying. Preemptive schedulers are better than your whole computer freezing. High level languages beat programming in assembly. Its just hard to say for sure how history will look back on any particular piece of tech in 20 years time. Fortran lost to C, even though its better in some ways. I think C++ / Java style OO will die a slow death, in favour of data oriented design and interfaces / traits (Go / Rust style). I could be wrong, but thats my bet. > I think it’s obvious that anyone passing around structs that contain data and functions that act on that data is the same concept as passing around objects. I hear what you're saying - but there's some important differences about the philosophy of how we conceptualise our programs. OO encourages us to think of nouns that "do" some verb. Using structs and functions (like C and Rust) feels much more freeform to me. Yegge said it better: https://steve-yegge.blogspot.com/2006/03/execution-in-kingdom-of-nouns.html https://steve-yegge.blogspot.com/2006/03/execution-in-kingdo... But lets see in 20 years. Maybe OO will be back, but I doubt it. I think if we can learn anything from the history of CS, its that functional programming had basically all the right ideas 40 years ago. Its just taking the rest of us decades to notice.
- eru 1y ago> Bit by bit, we're finally starting to win the fight against people who think pointless abstraction will make their software better. Of course, in the functional programming community we know that it is pointfree abstraction that makes your software better. https://wiki.haskell.org/Pointfree https://wiki.haskell.org/Pointfree (Please pardon the pun.)
- deleted 1y ago[deleted]
- badmintonbaseba 1y ago> Usually, the calls were via some abstract interface with a single implementor What's described here is over-generic code, instead of KISS and just keeping an eye on extensibility instead of generalizing ahead of time. This can happen in any paradigm.
- josephg 1y agoWe're all flavoured by our experience. You can for sure make a mess with flat C-style code that uses structs and global functions. But whenever I've seen a mess in C, its a sort of "lego on the floor" type of mess. Code is everywhere, but all the pieces are uniquely named and mostly self contained. Classes - and class hierarchies - really let you go to town. I've seen codebases that seem totally impossible to get your head around. The best is when you have 18 classes which all implicitly or explicitly depend on each other. In that case, just starting the program up requires an insane, fragile dance where lots of objects need to be initialized in just the perfect order, otherwise something hits a null pointer exception in its initialization code. You reorder two lines in a constructor somewhere and something on the other side of your codebase breaks, and you have no idea why. For some reason I've never seen anyone make that kind of mess just using composition. Maybe I just haven't been around long enough.
- jollyllama 1y agoYeah, I have seen things like you describe. But I have also seen the same code, copy-pasted a dozen times throughout a codebase and modified over years. That is a much worse situation; the links between the abstractions still exist without the inheritance, but now they are untraceable. At least with inheritance there are links between the methods and classes for you to follow. Without it, you've got to crawl the entire codebase to find these things. OOP is easily the lesser of the two evils; without it, you're doomed to violate DRY in ways that will make your project unmaintainable. I would even go so far as to argue that a small team of devs can learn an OOP heirarchy and work with it indefinitely, but a similar small team will drown in maintenance overhead without OOP and inheritance. This is highly relevant as we head into an age of decreased headcounts. This style of abandoning OOP will age poorly as teams decrease in size. Keeping to the DRY principle is also more valuable in the age of AI when briefer codebases use up fewer LLM tokens.
- josephg 1y ago> OOP is easily the lesser of the two evils; without it, you're doomed to violate DRY in ways that will make your project unmaintainable. Inheritance isn't the only way to avoid duplicating code. Composition works great - and it results in much more maintainable code. Rust, for example, doesn't have class based inheritance at all. And the principle of DRY is maintained in everything I've made in it. And everything I've read by others. Its composition all the way down, and it works great. Go is just the same. If anything, I think if you've got a weak team it makes even more sense to stick to composition over inheritance. The reason is that composition is easier to read and reason about. You don't get "spooky action from a distance" when you use composition, since a struct is made up of exactly the list of fields you list. Nothing more, nothing less. There's no overridden methods and inherited fields to worry about.
- jollyllama 1y agoI've experimented with GoLang and found the lack of inheritance to be crippling for cases when I want to set a pattern in the code that is to be easily used by other devs with minimal training and a shared definition of behavior. That said, I truly think some mix of inheritance and composition is probably best to avoid the situations we're describing.
- sumtechguy 1y agoAh you have used spring/spring boot I see. That thing. It has humbled me. I didnt know you could do that much abstraction.
- stickfigure 1y ago> The reason is that inheritance is almost always a bad idea in practice. It's just slightly too strong of a statement. I'm working in a very large Spring codebase right now, with a lot of horrible inheritance abuse (seriously, every component extended common hierarchy of classes that pulled in a ton of behavior). I suspect part of the reason is the Spring context got out of control, and the easiest way to reliably "inject" behavior is by subclassing. Terrible. On the other hand, inheritance is sometimes the most elegant solution to a problem. I've done this at multiple companies: Payment + PayPalPayment + StripePayment Sometimes you have data (not just behavior!) that genuinely follows an IS-A relationship, and you want more than just interface polymorphism. Yes you can model this with composition, but the end result ends up being more complex and uglier. It doesn't have to be all one or the other. But I agree, it should be mostly composition.
- WesolyKubeczek 1y agoThere used to be times when language-level composition did not exist, so inheritance was practically all you had. There used to be ugly hacks to implement mix-ins, for example, in PHP (first versions of Symfony used them and did their best to make them not ugly, but they had to devote a whole chapter on how to do them right anyway). I suspect a lot of contention comes from those times — and from the fact that even when you can do better, many folks still have the muscle memory wired to "if inheritance is the only tool you have, everything looks like a subclass". I like languages where I can have both, and where the language authors are not trying to preach at me.
- roguecoder 1y agoThat is a great example! Abstraction is most useful when it captures the way several things are more-specific versions of a more general thing. At that point it's not just about the functionality: it communicates to the reader. Anyone coming in can now easily answer the question, "what kinds of payments exist?"
- roguecoder 1y ago"But there's a reason the crowd is moving against inheritance" Yep: it requires skills that aren't taught in schools or exercised in big companies organized around microservices. We've gone back to a world where most developers are code monkeys, converting high-level design documents into low-level design documents into code. That isn't what OOP is good for: OOP is good for evolving maintainable, understandable, testable, expressive code over time. But that doesn't get you a promotion right now, so why would engineers value it?
- josephg 1y ago> That isn't what OOP is good for: OOP is good for evolving maintainable, understandable, testable, expressive code over time. Whoa that’s quite the claim. Most large projects built heavily on OO principles I’ve seen or worked on have become an absolute unmaintainable mess over time, with spider webs of classes referencing classes. To say nothing of DI, factoryfactories and all the rest. I believe you might have had some good experiences here. But I’m jealous, and my career doesn’t paint the same rosy picture from the OO projects I’ve seen. I believe most heavily OO projects could be written in about 1/3 as many lines if the developers used an imperative / dataflow oriented design instead. And I’m not just saying that - I’ve seen ports and rewrites which have born out around that ratio. (And yes, the result is plenty maintainable).
- osigurdson 1y agoHorrible code is a constant that will not be fixed by not using inheritance.