roenxi 3 days ago

I'm not sold the evidence is there to show inheritance is a good idea - it basically says that constructors, data storage and interfaces need to be intertwined. That isn't a very powerful abstraction, because they don't need to be and there isn't an obvious advantage from doing so over picking up the concepts separately as required. And inheritance naturally suggests grouping interfaces into a tree in the way that seems of little value because in practice a tree probably doesn't represent the fundamental truth of things. Weird edge cases like HTTP over non-TCP protocols or rendering without screens start throwing spanners into a tree of assumptions that never needed to be made and pull the truth-in-code away from the truth-in-world.

All that makes a lot of sense if it was introduced as a performance hack rather than a thoughtfully designed concept.

  • osigurdson 3 days ago

    Yeah ya... everyone likes to go on and on about how inheritance is the root of all evil and if you just don't use it, everything will be fine. Sorry, it won't be fine. Your software will still be a mess unless it is small and written three times by the same person who knows what they are doing.

    The bottom line is, no one ever really used inheritance that much anyway (other than smart people trying to outsmart themselves). People created AbstractFactoryFactoryBuilders not because they wanted to, but because "books" said to do stuff like and people were just signaling to the tribe.

    So now, we are now all signaling to the new tribe that "inheritance is bad" even though we proudly created multiple AFFs in the past. Not very original in my opinion since Go and Rust don't have inheritance. The bottom line is, most people don't have any original opinions at all and are just going with whatever seems to be popular.

    • josephg 3 days 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 3 days ago

        I 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 3 days 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 */ }
              }
          • OskarS 2 days ago

            > The only one that springs to mind is a View hierarchy in UI libraries.

            I'd like to generalize that a little bit and say: graph structures in general. A view hierarchy is essentially a tree, where each node has a bunch of common bits (tree logic) and a bunch of custom bits (the actual view). There are tons of "graph structures" that fit that general pattern: for instance, if you have some sort of data pipeline DAG where data comes in on the left, goes out on the right, and in the middle has to pass through a bunch of transformations that are linked in some kind of DAG. Inheritance is great for this: you just have your nodes inherit from some kind of abstract "Node" class that handles the connection and data flow, and you can implement your complex custom behaviors however you want and makes it very easy to make new ones.

            I'm very much in agreement that OOP inheritance has been horrendously overused in the 90s and 00s (especially in enterprise), but for some stuff, the model works really well. And works much better than e.g. sum types or composition or whatever for these kinds of things. Use the right tool for the right job, that's the central point. Nothing is one-size-fits-all.

            • aatd86 2 days ago

              turns out that using composition and polymorphism is usually simpler than inheritance in such cases.

          • widdershins 2 days ago

            > But even then, I notice React (& friends) have all moved away from this approach. Modern web development usually makes components be functions.

            But what do those functions return? Oh look, it's DOM nodes, which are described by and implemented with inheritance.

            I would agree that view hierarchies in UI libraries are one of the primary use-cases for inheritance. But it's a pretty big one.

            • josephg 2 days ago

              > But what do those functions return? Oh look, it's DOM nodes, which are described by and implemented with inheritance.

              Well of course. React builds on what the browser provides. And the DOM has been defined as a class hierarchy since forever. But react components don’t inherit from one another. If the react devs could reinvent the DOM, I think it would look very different than it looks today.

          • esailija 3 days ago

            The problem is of course that there is no useful default behavior you can define when the trait is so isolated and generic.

            • josephg 2 days ago

              It 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

              Source: https://doc.rust-lang.org/src/std/io/mod.rs.html#1596-1935

              • RHSeeger 2 days ago

                > First, traits don't define any fields.

                How does one handle cases where fields are useful? For example, imagine you have a functionality to go fetch a value and then cache it so that future calls to get that functionality are not required (resource heavy, etc).

                    // in Java because it's easier for me
                    public interface hasMetadata {
                        Metadata getMetadata() {
                            // this doesn't work because interfaces don't have fields
                            if (this.cachedMetadata == null) {
                                this.cachedMetadata = generateMetadata();
                            }
                            return this.cachedMetadata;
                        }
                        // relies on implementing class to provide
                        Metadata fetchMetadata(); 
                
                    }
                • Klonoar 2 days ago

                  Getters and setters that get specified by the implementing type.

                  • RHSeeger 2 days ago

                    But then you have the getters, setters, and field on every class that implements the functionality. It works, sure, it just feels off to me. This is code that will be the same everywhere, and you're pulling it out of the common class and implementing it everywhere.

                  • josephg 2 days ago

                    Yep. Or ... don't put that in the interface at all. It looks like an implementation concern to me.

                    • RHSeeger 2 days ago

                      But if there's a lot of classes that implement the same thing, then not duplicating code makes sense. And saying "it's an implementation detail" leads to having the same code in a bunch of different classes. It feels very similar to the idea of default implementations to me; when the implementation will be the same everywhere, it makes sense to have it in one place.

                      • josephg 2 days ago

                        So to be clear about your example: You have a whole lot of different - totally distinct - types of things, which all need to have the same logic to cache HTTP requests? Can you give some examples of these different types you're creating? Why do you have lots of distinct types that need exactly the same caching logic?

                        It sounds like you could solve that problem in a lot of different ways. For example, you could make an HTTP client wrapper which internally cached responses. Or make a LazyResource struct which does the caching - and use that in all those different types you're making. Or make a generic struct which has the caching logic. The type parameter names the special individual behaviour. Or something else - I don't have enough information to know how I'd approach your problem.

                        Can you describe a more detailed example of the problem you're imagining? As it is, your requirements sound random and kind of arbitrary.

                        • RHSeeger 2 days ago

                          From a very modified version of something I was working on recently, but with the stuff I couldn't do actually done here (and non-functionality code because of that, but is shows the idea)

                              public interface MetadataSource {
                                  Metadata metadata = null;
                          
                                  default Metadata getMetadata() {
                                      if (metadata == null) {
                                          metadata = fetchMetadata();
                                      }
                                      return metadata;
                                  }
                                  
                                  // This can be relatively costly
                                  Metadata fetchMetadata();
                              }
                          
                              public class Image implements MetadataSource {
                                  public Metadata fetchMetadata() {
                                      // goes to externally hosted image to fetch metadata
                                  }
                              }
                          
                              public class Video implements MetadataSource {
                                  public Metadata fetchMetadata() {
                                      // goes to video hosting service to get metadata
                                  }
                              }
                          
                              public class Document implements MetadataSource {
                                  public Metadata fetchMetadata() {
                                      // goes to database to fetch metadata
                                  }
                              }
                          
                          Each of the above have completely different ways to fetch their metadata (ex, Title and Creator), and of them has different characteristics related to the cost of getting that data. So, by default, we want the interface to cache the result so that the

                          1. The thing that _has_ the metadata only needs to know how to fetch it when it's asked for (implementation of fetchMetadata), and it doesn't need to worry about the cost of doing so (within limits of course)

                          2. The things that _use_ the metadata only need to know how to ask for it (getMetadata) and can assume it has minimal cost.

                          3. Neither one of those needs to know anything about it being cached.

                          I had a case recently where I needed to check "does this have metadata available" separate from "what is the metadata". And fetching it twice would add load.

                          • josephg 2 days ago

                            Here's my take on implementing this in rust. I made a trait for fetching metadata, that can be implemented by Image, Video, Document, etc:

                                trait MetadataSource {
                                    fn fetch_metadata(&self) -> Metadata;
                                }
                                impl MetadataSource for Image { ... } 
                                impl MetadataSource for Video { ... } 
                                impl MetadataSource for Document { ... }
                            
                            And a separate object which stores an image / video / document alongside its cached metadata:

                                struct ThingWithMetadata<T> {
                                    obj: T, // Assuming you need to store this too?
                                    metadata: Option<Metadata>
                                }
                            
                                impl<T: MetadataSource> ThingWithMetadata {
                            
                                    fn get_metadata(&self) -> &Metadata {
                                        if self.metadata.is_none() {
                                            self.metadata = Some(self.obj.fetch_metadata());
                                        }
                                        self.metadata.as_ref().unwrap()
                                    }
                                }
                            
                            Its not the most beautiful thing in the world, but it works. And it'd be easy enough to add more methods, behaviour and state to those metadata sources if you want. (Eg if you want Image to actually load / store an image or something.)

                            In this case, it might be even simpler if you made Image / Video / Document into an enum. Then fetch_metadata could be a regular function with a match expression (switch statement).

                            If you want to be tricky, you could even make struct ThingWithMetadata also implement MetadataSource. If you do that, you can mix and match cached and uncached metadata sources without the consumer needing to know the difference.

                            https://play.rust-lang.org/?version=stable&mode=debug&editio...

                            • zozbot234 2 days ago

                              Isn't this essentially the generic typestate pattern in Rust? In my view there is a pretty obvious connection between that particular pattern and how other languages implement OO inheritance, though in all fairness I don't think that connection is generally acknowledged.

                              (For one thing, it's quite obvious to see that the pattern itself is rather anti-modular, and the ways generic typestate is used are also quite divergent from the usual style of inheritance-heavy OO design.)

                            • RHSeeger 2 days ago

                              When you call myImageInstance.fetchMetadata, what does it do? I don't know rust, so it's not clear to me how the value gets cached.

                              • josephg 2 days ago

                                In this example, ThingWithMetadata does the caching. image.fetch_metadata fetches the image and returns it. It’s up to the caller (in ThingWithMetadata) to cache the returned value.

                                • RHSeeger 2 days ago

                                  But part of the goal is to not need the caller to cache it. Nor have the class that knows how to fetch it need to know how to cache it either. The responsibility of knowing how to cache the value is (desired to be) in the MetadataSource interface.

                                  • josephg 2 days ago

                                    The rule is that you can't cache a value in an interface, because interfaces don't store data. You need to cache a value in a struct somewhere. This implementation wraps items (like images) in another struct which stores the image, and also caches the metadata. Thats the point of ThingWithMetadata. Maybe it should instead be called WithCachedMetadata. Eg, WithCachedMetadata<Image>.

                                    You can pass WithCachedMetadata around, and consumers don't need to understand any of the implementation details. They just ask for the metadata and it'll fetch it lazily. But it is definitely more awkward than inheritance, because the image struct is wrapped.

                                    As I said, there's other ways to approach it - but I suspect in this case, using inheritance as a stand-in for a class extension / mixin is probably going to always be your most favorite option. A better approach might be for each item to simply know the URL to their metadata. And then get your net code to handle caching on behalf of the whole program.

                                    It sounds like you really want to use mixins for this - and you're proposing inheritance as a way to do it. The part of me which knows ruby, obj-c and swift agrees with you. I like this weird hacky use of inheritance to actually do class mixins / extensions.

                                    The javascript / typescript programmer in me would do it using closures instead:

                                        function lazyResource(url) {
                                          let cached = null
                                          return async () => {
                                            if (cached == null) cached = await fetch(url)
                                            return cached
                                          }
                                        }
                                    
                                        // ...
                                        const image = {
                                          metadata: lazyResource(url)
                                        }
                                    
                                    Of all the answers, I think this is actually my favorite solution. Its probably the most clear, simple and expressive way to solve the problem.
                                    • RHSeeger 2 days ago

                                      > The rule is that you can't cache a value in an interface, because interfaces don't store data.

                                      Right, but the start of where I jumped into this thread was about the fact that there are places where fields would make things better (specifically in relation to traits, but interfaces, too). And then proceeding to discuss a specific use case for that.

                                      > A better approach might be for each item to simply know the URL to their metadata.

                                      Not everything is a coming from a url and, even when it is, it's not always a GET/REST fetch.

                                      > but I suspect in this case, using inheritance as a stand-in for a class extension / mixin is probably going to always be your most favorite option

                                      Honestly, I'd like to see Java implement something like a mixin that allows adding functionality to a class, so the class can say "I am a type of HasAuthor" and everything else just happens automatically.

                          • svieira 2 days ago

                            One way you could fix this with composition is:

                                class CachedMetadataSource implements MetadataSource {
                                  CachedMetadataSource(MetadataSource uncachedSource) {}
                                  Metadata getMetadata() {
                                    if (metadata == null) {
                                      metadata = uncachedSource.getMetadata();
                                    }
                                    return metadata;
                                  }
                                }
                            • RHSeeger 2 days ago

                              I don't see how that solves the problem. It seems like Video will need to keep it's own copy of CachedMetadaSource, which points back to itself, and go through that access it's metadata in the getMetadata implementation it makes available to it's users. At that point, it might as well just cache the value itself without the extra hoops. The difficult part isn't caching the value, it's preventing every class that implements MetadataSource from having to do so.

                              • svieira 2 days ago

                                It would be the other way around. You wouldn't pass around the underlying suppliers directly, you'd wrap them. But if you must have state _and_ behavior, then `abstract class` is your friend in Java (while in Scala traits can have fields and constructors, so there is no problem).

            • jjmarr 2 days ago

              Don't mix implementation and interface inheritance.

          • osigurdson 2 days ago

            The commenter used inheritance and thought it was fine. Probably not necessary to re-write in Rust just to be able to say that it doesn't use inheritance while being functionally the same thing.

          • graealex 2 days ago

            > And yes, javascript supports many kinds of inheritance

            Funny you mention it, since JavaScript has absolutely no concept of contracts, which is one of the most important side-effects of inheritance. Especially not at compile time, but even at runtime you can compose objects willy-nilly, pass them anywhere, and the only way to test if they adhere to some kind of trait is calling a method and hoping for the best.

            At least that had been the case till ES6 came around, but good luck finding anyone actually using classes in JavaScript. Mainly because it adds near-zero benefits, basically just the ability to overwrite method behavior without too much trickery.

            • josephg 2 days ago

              Yes. JavaScript is an incredibly dynamic language. If you don’t like that, don’t use it.

          • pixelfarmer 2 days ago

            I will tell you one example with inheritance: The Linux kernel.

            • pjc50 2 days ago

              How does that work in a language without inheritance?

              (yes, I guess it's the fake vtable of structure full of pointers)

              • nyrikki 2 days ago

                Structure composition is a form of inheritance.

        • unscaled 2 days ago

          Inheritance is not the only way to share behavior across different implementations — it'a just the only way available in the traditional 1990s crop of static OOP languages like C++, Java and C#.

          There are many other ways to share an implementation of a common feature:

          1. Another comment already mentioned default method implementations in an interface (or a trait, since the example was in Rust). This technique is even available in Java (since Java 8), so it's as mainstream as it gets.

          The main disadvantage is that you can have just one default implementation for the stop() method. With inheritance you could use hierarchies to create multiple shared implementations and choose which one your object should adopt by inheriting from it. You also cannot associate any member fields with the implementation. On the bright side, this technique still avoids all the issues with hierarchies and single and multiple inheritance.

          2. Another technique is implementation delegation. This is basically just like using composition and manually forwarding all methods to the embedded implementer object, but the language has syntax sugar that does that for you. Kotlin is probably the most well-known language that supports this feature[1]. Object Pascal (at least in Delphi and Free Pascal) supports this feature as well[2].

          This method is slightly more verbose than inheritance (you need to define a member and initialize it). But unlike inheritance, it doesn't requires forwarding the class's constructors, so in many cases you might even end up with less boilerplate than using inheritance (e.g. if you have multiple overloaded constructors you need to forward).

          The only real disadvantage of this method is that you need to be careful with hierarchies. For instance, if you have a Storage interface (with the load() and store() methods) you can create EncryptedStorage interface that wraps another Storage implementation and delegates to it, but not before encrypting everything it sends to the storage (and decrypting the content on load() calls). You can also create a LimitedStorage wrapper than enforces size quotas, and then combine both LimitedStorage and EncryptedStorage. Unlike traditional class hierarchies (where you'd have to implement LimitedStorage, EncryptedStorage and LimitedEncryptedStorage), you've got a lot more flexibility: you don't have to reimplement every combination of storage and you can combine storages dynamically and freely. But let's assume you want to create ParanoidStorage, which stores two copies of every object, just to be safe. The easiest way to do that is to make ParanoidStorage.store() calls wrapped.store() twice. The thing you have to keep in mind, is that this doesn't work like inheritance: For instance, if you wrap your objects in the order EncryptedStorage(ParanoidStorage(LimitedStorage(mainStorage))), ParanoidStorage will call LimitedStorage.store(). This is unlike the inheritance chain EncryptedStorage <- ParanoidStorage <- LimitedStorage <- BaseStorage, where ParanoidStorage.store() will call EncryptedStorage.store(). In our case this is a good thing (we can avoid a stack overflow), but it's important to keep this difference in mind.

          3. Dynamic languages almost always have at least one mechanism that you can use to automatically implement delegation. For instance, Python developers can use metaclasses or __getattr__[3] while Ruby developers can use method_missing or Forwaradable[4].

          4. Some languages (most famously Ruby[5]) have the concept of mixins, which let you include code from other classes (or modules in Ruby) inside your classes without inheritance. Mixins are also supported in D (mixin templates). PHP has traits.

          5. Rust supports (and actively promotes) implementing traits using procedural macros, especially derive macros[6]. This is by far the most complex but also the most powerful approach. You can use it to create a simple solution for generic delegation[7], but you can go far beyond that. Using derive macros to automatically implement traits like Debug, Eq, Ord is something you can find in every codebase, and some of the most popular crates like serde, clap and thiserror rely on heavily on derive.

          [1] https://kotlinlang.org/docs/delegation.html

          [2] https://www.freepascal.org/docs-html/ref/refse48.html

          [3] https://erikscode.space/index.php/2020/08/01/delegate-and-de...

          [4] https://blog.appsignal.com/2023/07/19/how-to-delegate-method...

          [5] https://ruby-doc.com/docs/ProgrammingRuby/html/tut_modules.h...

          [6] https://doc.rust-lang.org/reference/procedural-macros.html#d...

          [7] https://crates.io/crates/ambassador

          • roguecoder 2 days ago

            To my mind, the challenge is not "sharing behavior"; it is "sharing behavior in a way that capture human-understandable semantics and make code easier to reason about instead of harder."

            I suspect part of the problem of inheritance is that it is a way to share behavior that some humans, especially visual thinkers who understand VMTs, find easy to reason about.

            In my experience verbal thinkers struggle with inheritance, because it requires jumping between levels of abstraction and they aren't thinking in terms of semantic units. I have found that books like Refactoring can help bridge the gap, but we have to identify it as a gap to be bridged and people have to want to learn this new skill.

            And then on the flip side you have people who try to use it just as a way to de-dupe code, even when it doesn't reflect a meaningful semantic unit.

            • josephg 2 days ago

              > In my experience verbal thinkers struggle with inheritance, because it requires jumping between levels of abstraction and they aren't thinking in terms of semantic units.

              This is too dismissive of the criticism. The problem with inheritance is it makes control flow harder to understand and it spreads your logic all over a bunch of classes. Ironically, inheritance violates encapsulation - since a base class is usually no longer self contained. Implementation details bleed into derived classes.

              The problem isn’t “verbal thinkers”. I can think in OO just fine. I’ve worked in 1M+ line of code Java projects, and submitted code to chrome - which last time I checked is a 30M loc C++ project. My problem with OO is that thinking about where any given bit of code is distracts me from what the code is trying to do. That makes me less productive. And I’ve seen that same problem affect lots of very smart devs, who get distracted building a taxonomy in code instead of solving actual problems.

              It’s not a skills problem. Programming is theory building. OO seduces you into thinking the best theory for your software is a bunch of classes which inherit from each other, and which reference each other in some tangled web of dependencies. With enough effort, you can make it work. But it almost always takes more effort than straightforward dataflow style programming to model the same thing.

              • roguecoder a day ago

                I do not believe "it makes the control flow harder to understand" is as universal as you claim. If used badly any flow tool (including if-statements) can be be confusing. But "it can be complicated" doesn't mean we shouldn't use the tool when it is appropriate. One of the reasons I like Java Enums is because they provide much more structured guidance on what communicative inheritance looks like.

                But we may also disagree on what "productive" means in the context of writing software.

                The "taxonomy of code" you are dismissing is I believe what Fred Brooks describes as the "essential tasks" of programming: "fashioning of the complex conceptual structures that compose the abstract software entity".

                It's not that I don't sympathize with your concern: being explicit and clear about "what the code is trying to do" is why TDD is popular among OOP programmers. But the step after "green" is "refactor", where the programmer stops focusing on what the code is trying to do and refines the taxonomy of the system that implements those tasks.

        • giamma 2 days ago

          To me (as a Java programmer) inheritance is very useful to reuse code and avoid copy paste. There many cases in which decorators or template methods are very useful and in general I find it "natural" in the sense that the concepts of abstraction and specialization can be found in plenty of real world examples (animals, plants, vehicles etc etc).

          As usual there is no silver bullet, so it's just a tool and like any other tool you need to use it wisely, when it makes sense.

          • ffsm8 2 days ago

            As a full stack developer who's current job is mostly Java on the backend - at least for the last 8 yrs: I'm not aware of anything you would lose by switching to interfaces with default implementations over inheritance... And that's the usual argument: use composition over inheritance.

            • yorwba 2 days ago

              But would switching to interfaces with default implementations fix any of the complaints that people have about inheritance? In my mind, they're pretty much equivalent, so it seems to me that anything you can do with inheritance that people complain about, you could also do with interfaces and complain about it in the same way.

              • ffsm8 2 days ago

                The biggest difference are

                1. A class can be composed out of multiple interfaces, making them more like mixins/traits etc vs inheritance, which is always a singular class

                2. The implementation is flat and you do not have a tree of inheritance - which was what this discussion was about. This obviously comes with the caveat that you don't combine them, which would effectively make it inheritance again.

        • esailija 3 days ago

          Yeah there can be a ton of derivative and convenience methods that would either have to be duplicated in all implementations or even worse duplicated at call sites.

          Call them interfaces with default implementations or super classes, they are the same thing and very useful.

      • stickfigure 2 days 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 2 days ago

          There 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 2 days ago

          That 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?"

      • wolvesechoes 2 days 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.

        • bigstrat2003 2 days ago

          I definitely agree that the crusade against inheritance is just a fad and not based on good reasoning. Every time people say "inheritance is garbage that people only use because they learned it in school" it pains me because it's like, really? You can't imagine that it's because those people have thought about the options and concluded that inheritance is the best way to model the problem they are facing?

          Contrary to what the hype of the 90s said, I don't think OOP is the ultimate programming technique which will obsolete all others. But I think that it's equally inaccurate to make wild claims about how OOP is useless garbage that only makes software worse. Yes, you can make an unholy mess of class structures, but you can do that with every programming language. The prejudice some people have against OOP is really unfounded.

          • roguecoder 2 days ago

            I think there is a tendency in our industry to externalize imposter syndrome, blaming the tools rather than thinking "huh, I don't understand OOP yet."

            Which doesn't mean everyone has to learn to understand OOP, but just because one person doesn't want to doesn't mean no one should.

        • ab5tract 2 days ago

          I’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 2 days ago

            I 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-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.

            • osigurdson 2 days ago

              Although Java/C# make you put functions in a class, you aren't compelled to think of a class as a "noun". Just call it "Utils" or something like that. A class is just a thing that you can put functions and / or data in. Use that however you want.

              • neonsunset 2 days ago

                if anything, in C#, you can import the entire class as `using static MyFunctions;` and make such functions top-level. Well, usually you write an extension method instead since most functions act on some form of data but you get the idea.

                (can also be imported globally with 'global using static ..' in a usings file)

                • josephg 2 days ago

                  Yeah, the problem with OO isn’t really in the languages. The problem is in the community, and what people consider “best practice”. C#, Java and C++ are all arguably multi-paradigm languages. They give you a lot of flexibility in how you structure your code. C# and C++ support value types. Modern Java has great support for a lot of FP concepts too.

                  So I agree with you. You can write good C# if you want to. The problem is that a lot of people - for some strange reason - actively choose to make their programs heavily OOP.

                  • osigurdson 2 days ago

                    Maybe we need to tease "community" apart from language. Let's have Java / C# "A" people (who need at least 10 levels of inheritance, gotta use DI, insist on every character of SOLID (and actually remember and care about the Liskov substitution principle - and insist that it wasn't chosen simply because it starts with "L" and makes the acronym sound better) and have never written any code that added any value - only frameworks. Then we can have Java / C# "B" people that care about allocations, hate DI, avoid inheritance, know when they are messing up cache line hits and even feel slightly bad a about using generics.

                    Something like that, pick your tribe or, even better, be an individual and do whatever (TF) you want.

                    • josephg 2 days ago

                      Yep. That’s why I prefer to criticise OOP (and in particular, inheritance). Not specific languages.

                      I met this old guy at a conference one, ~15 years ago. He said he didn’t get why people say Java is slow. His Java, he said, runs just as fast as C. I asked him to show me his code - and I’m so glad I did. It was amazing. He did everything in one big static class, and treated Java as if it were a funny way to write C. He ignored almost the entire standard library. No wonder his code ran fast. It was basically JIT-compiled C code.

                      Java isn’t the problem. “Java best practices” are the problem. It’s a culture thing. Likewise, can write heavily OOP code in C if you really put your mind to it and write your own struct full of function pointers. But it’s not in culture of the C community to overuse that design.

                    • neonsunset a day ago

                      > even feel slightly bad a about using generics.

                      Hey, struct generics are the go-to tool for zero-cost abstractions in .NET! No need to feel bad about them :)

              • immibis a day ago

                The word "module" comes to mind. A class in Java can be viewed as a software module, of which more than one instance can be created. Sometimes, this is even the best way to view the class. Other times, it's better to view it as a class representing some noun.

                A thing like a "comparator" or an "XYZ factory" is not a domain noun, but rather a pluggable code module.

      • lelanthran 3 days 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 2 days ago

          Java 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 2 days ago

            Java 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 and https://www.jot.fm/issues/issue_2002_05/article1/

            • lmm 2 days ago

              Objective-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.

              • lelanthran 2 days ago

                > 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.

                Your distinction between "first class support for interfaces" and "C++ support for interfaces" looks like an artificial one to me.

                Other than not requiring the keyword "interface", what is it about the C++ way of creating an interface that makes it not "first class support"?

              • kgeist 2 days 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.

                • gpderetta 2 days ago

                  The main reason (other than self-documentation) that some other languages separate interfaces form normal classes is that they only support multiple inheritance for interfaces.

                  C++ doesn't have this restriction, so interfaces would add very little.

                  • colejohnson66 2 days ago

                    Because multiple inheritance causes the diamond problem. C# forces you to solve it by declaring one method as the "canonical" and the others as "explicit interface implementations" (only accessible if the variable/receiver is typed as that interface).

                    • layer8 2 days ago

                      The diamond problem strictly speaking only has to be one when the common base class has constructor arguments. While a Java-style interface construct makes it easy to prevent, it also imposes much stronger restrictions than the above. It would have been possible to only impose the above restriction. Yes, there are failure cases with separate compilation, but the way Java dynamically loads classes, that would be similar to how when the JVM loads a class file that is supposed to be an interface, it discovers that instead it has been changed to a class.

              • pasc1878 9 hours ago

                Java's object model is based on Objective-C's so a direct descendant and Objective-C's object model is based on Smalltalk so there is a direct connection there.

              • layer8 2 days ago

                Java having an explicit ”interface” construct is one thing I didn’t like about it, because it muddles the notion of a class implicitly having an interface (a notion that clearly exists in C and C++, by way of header files if nothing else) with that construct, while on the other hand there is no a-priori reason to have a distinction between Java’s interfaces and pure abstract classes. Both specify an interface to be implemented. And Java 8+ muddles its concept further by allowing default methods and static members.

                The important thing is to distinguish between interface and implementation, and that is relevant to any class, whether it implements a separately defined interface or not.

                • pasc1878 a day ago

                  Java (following Objective-C) does need a differentiation between interface and pure abstract class - this is because it is single inheritance - a class can have any one class to inherit from but it can have many interfaces.

                  • layer8 a day ago

                    That doesn’t follow. The restriction could have been defined in terms of allowing at most one parent class to be non-pure-abstract. And there are lesser restrictions hat would have been conceivable as well. Java is single-inheritance only in the sense of the particular interface–class distinction it makes. For example, since Java 8 you can inherit method implementations from multiple interfaces.

      • eru 2 days 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

        (Please pardon the pun.)

      • eloisius 2 days ago

        As 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 2 days ago

          And Rust's traits can sort-of inherit from each other.

          • josephg 2 days ago

            I'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 2 days 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 2 days ago

              That 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.

              • josephg 2 days ago

                Thanks for the diagnosis but no. I’ve had these opinions for years - since long before rust came along. If we had this conversation a decade ago, I might have made the same argument on the back of Java’s interfaces or obj-c’s protocols - which are both more or less the same concept.

      • sumtechguy 2 days ago

        Ah you have used spring/spring boot I see. That thing. It has humbled me. I didnt know you could do that much abstraction.

      • roguecoder 2 days 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 2 days 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).

      • badmintonbaseba 2 days 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 2 days ago

          We'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.

      • silisili 3 days ago

        I 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 3 days 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.

      • jollyllama 2 days ago

        Yeah, 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 2 days 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 2 days ago

            I'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.

            • josephg 2 days ago

              I suspect that an experienced golang programmer could solve whatever abstraction problem you have using Go's tools of composition and interfaces. Chatgpt could probably get you started too, if you prompt it in the right way.

              Generally, don't treat Go as if its some bad imitation of C++ or Java. Its a different language. Like all languages, idiomatic Go is its own thing. It looks different to idiomatic Ruby or Javascript or C++ or Perl.

              I think of programming languages kind of like pieces of wood. Each language has its own "grain" that you need to follow when you work. If you try and force any programming language into acting like its something else, you're going against the grain of the language. You'll need to work 10x harder to get anywhere if you try to work like that. Spend more time learning.

            • osigurdson 2 days ago

              It is possible, just look at all of the go packages out there. Also, maybe you don't need to wrap it up as tightly as you think. The "other devs" will use it wrong anyway.

        • istjohn 2 days ago

          I think you have the consequences of AI exactly backwards. AI provides virtual headcount and will vastly increase the ability of small teams to manage sprawling codebases. LLM context lengths are already on the order of millions of tokens. It takes a human days of work to come to grips with a codebase an LLM can grok in two seconds.

          The cost of working with code is much lower with LLMs than with humans and it's falling by an order of magnitude every year.

          • jollyllama 2 days ago

            So if you've got a data object, defined in multiple places in a sprawling codebase, that you want to change, are you going to trust the LLM to find them all, and not miss a single one?

            • josephg 2 days ago

              Why is your data object defined in multiple places in your codebase? And why aren't you using your IDE to change them all at once?

              • jollyllama 2 days ago

                > Why is your data object defined in multiple places in your codebase?

                Because that's the negation of my premise which you disagreed with: "Keeping to the DRY principle is also more valuable in the age of AI when briefer codebases use up fewer LLM tokens."

                > And why aren't you using your IDE to change them all at once?

                It sounds like you're assuming that they're all defined in the same way that you can catch them with a search.

          • actionfromafar 2 days ago

            So everyone is incentivized to increase sprawl until the equilibrium is found.

      • osigurdson a day ago

        Horrible code is a constant that will not be fixed by not using inheritance.

    • quietbritishjim 2 days ago

      > The bottom line is, no one ever really used inheritance that much anyway (other than smart people trying to outsmart themselves).

      Inheritance is most definitely used in many popular C++ libraries, e.g., protobuf::Message [1] (which is base class to all user message classes and also has its own base class of MessageLite) or QWidget [2] (which sits in a large class hierarchy) or tinyxml2::XMLNode (base class to other node types). These are honestly the first three libraries that I thought of that have a non-trivial collection of classes in them. They're all stateful base classes by the way, not pure interfaces. And remember, I'm not trying to justify whether these are good or bad designs, just the make the observation that inheritance certainly is well used in practice.

      (The fourth library I thought of with a reasonably complex collection of classes is Boost ASIO [4] which actually doesn't use inheritance. Instead it uses common interfaces to allow some compile-time polymorphism. Ironically, this is the only library in the list that I've been so unsatisfied with that I've written my own wrapper more than once for a little part of it: allowing auto-(re)connecting outbound and accepting incoming connections with the same interface. Guess what: I used inheritance!)

      [1] https://protobuf.dev/reference/cpp/api-docs/google.protobuf....

      [2] https://doc.qt.io/qt-6/qwidget.html

      [3] https://leethomason.github.io/tinyxml2/classtinyxml2_1_1_x_m...

      [4] https://www.boost.org/doc/libs/1_88_0/doc/html/boost_asio/re...

    • goeiedaggoeie 2 days ago

      >> People created AbstractFactoryFactoryBuilders not because they wanted to,

      I don't think this is accurate. people created factories like this because they were limited by interface bounds in the languages they were coding in and had to swap out behaviour at run or compile time for testing or configuration purposes.

    • falcor84 2 days ago

      > Your software will still be a mess unless it is small and written three times by the same person who knows what they are doing.

      100% this! And I've recently been wondering whether this is the right workflow for AI-assisted development: use vibe-coding to build the one that you plan to throw away [0], use that to validate your assumptions and implement proper end-to-end tests, then recreate it again once or more with AI asked to try different approaches, and then eventually throw these away too and more manually create "the third one".

      [0] "In most projects, the first system built is barely usable....Hence plan to throw one away; you will, anyhow." Fred Brooks, The Mythical Man-Month

    • throw4847285 2 days ago

      The reason people don't have original opinions is because it isn't worth it. The stakes are extremely low. How one chooses to write code is ultimately a matter of personal preference.

      The lower the stakes, the more dogmatic people become about their choices, because they know on some level it's a matter of taste and nothing more. Counterintuitively, it becomes even more tied to one's ego than the choices that actually have major consequences.

    • rixed 3 days ago

      I believe you just summed up 90% of popular wisdom about software engineering.

      With enough patience you will see many fads pass twice like a tide raising and falling. OOP, runtime typing, schema-less databases and TDD are the first to come to mind.

      I feel "self-describing" data formats and everything agile are fading already.

      Very few ideas stick, but some do: I do not expect GOTO to ever come back, but who knows where vibe coding will lead us :)

    • shadowgovt 2 days ago

      Objects are a pretty good abstraction for when you have data that represents, well, objects. In 3D graphics it's a very useful abstraction. Significantly less good when you're trying to model process, pipeline, or flow IMHO (I know there are some people who swear by them for anything they would bash together with UML first, and I just... Don't see it. I've used more than enough object-oriented flowchart-description languages to fundamentally disagree; charts are two-dimensional, text-represented code is one-dimensional, making the code "objects" doesn't fix that problem).

      (Probably also worth noting that high performance 3D graphics torture the object abstraction past recognizability, because maintaining those runtime abstractions costs resources that could be better spent slamming pixels into a screen).

    • ajuc 2 days ago

      > The bottom line is, no one ever really used inheritance that much anyway

      That's just false. Before Java abstract factory era there was already a culture of creating deep inheritance hierarchies in C++ code. Interfaces and design patterns (including factories) were adopted as a solution to that mess and as bad as they were - they were still an improvement.

    • tikhonj 2 days ago

      I've written a bunch of code in languages without inheritance per se—OCaml, Haskell, Rust—and things have been more than fine. Hell, I barely use any sort of subtyping! I definitely miss structural subtyping in Haskell and Rust on occasion, but even in those situations the code has never reduced to a thrice-written mess.

      I've also written some code that's gotten a lot of mileage out of inheritance, including multiple inheritance. Some of my Python abstractions would not have worked anywhere near as well as they did without it. But even then, I could build APIs at least as usable in languages without inheritance, as long as those languages had sufficient facilities for abstraction of their own. (Which OCaml, Haskell and Rust absolutely do!)

    • rayiner 2 days ago

      The problem is every library and framework uses a ton of class hierarchies with big inheritance trees.

    • karolinepauls 2 days ago

      > Your software will still be a mess

      Your software will still be a mess but a mess you can work with. Not a horror beyond comprehension. We should aim for workable mess.

      This is from experience working with both procedural/functional mess and OO mess.

    • cloogshicer 2 days ago

      I've long been searching for a concise example of "good" inheritance, can you recommend one?

  • williamdclt 2 days ago

    > Weird edge cases like HTTP over non-TCP protocols or rendering without screens start throwing spanners into a tree of assumptions that never needed to be made

    yes, but that's true of other abstractions too. Whether you use inheritance or not, you usually don't know what abstractions you need until you need them: even if you were using composability rather than inheritance, chances are that you'd have encoded assumptions that HTTP goes over TCP until you need to handle the fact that actually you need higher-level abstractions there.

    If you don't use inheritance, you switch to an interface (or a different interface) in your composition. If you did use inheritance, you stop doing so and start using composition. The latter is probably some more work but i don't think it's fundamentally very different.

  • kragen 3 days ago

    I'm on the fence about inheritance myself; I often regret having used it, and I never regret having not used it. On the other hand, it's awfully expedient. I designed and implemented a programming language called Bicicleta whose only argument-passing mechanism is inheritance, and I'm not sure that was a bad idea.

    The object-oriented part of OCaml, by the way, has inheritance that's entirely orthogonal to interfaces, which in OCaml are static types. Languages like Smalltalk and, for the most part, Python don't have interfaces at all.

    • igouy 2 days ago

      1992 "Interfaces and Specifications for the Smalltalk Collection Classes"

      https://dl.acm.org/doi/pdf/10.1145/141936.141938

      • kragen 2 days ago

        Very interesting work! It is an attempt to extract the interfaces that were in the minds of the implementors of the Smalltalk-80 system's collection classes, but which couldn't be expressed in the language itself, because it has no interface construct. That's what I meant by "Languages like Smalltalk (...) don't have interfaces at all."

        • igouy a day ago

          Don't have manifest types and don't have manifest interfaces.

          Someone has already referenced "Adding Dynamic Interfaces to Smalltalk" [0] and looking back there doesn't seem to be any kind of demonstration that use of interfaces makes software faster to develop or less error prone or... [1]

              [0] https://www.jot.fm/issues/issue_2002_05/article1/
              [1] https://www.cs.utexas.edu/~wcook/papers/OOPSLA89/interfaces.pdf
          • kragen 19 hours ago

            Unfortunately, in general, there is not much convincing proof that one way of developing software is better than another in such ways.

    • galbar 2 days ago

      Python has Protocols. They work like Go interfaces

      • kragen 2 days ago

        Sure, but for the most part people don't use them, because you don't have to; Python method calls are always potentially polymorphic, unlike Golang method calls.

    • echelon 3 days ago

      Rigid, "family tree"-style inheritance as in classical OOP is pretty much garbage. "A cow is a mammal is an animal" is largely useless for the day to day work we do except in extremely well-planned, large and elaborate ontologies -- something you typically only see in highly structured software like windowing systems. It just isn't useful for the majority of our work.

      "Trait/Typeclass"-style compositional inheritance as in Rust and Haskell is sublime. It's similar to Java interfaces in terms of flexibility, and it doesn't enforce hierarchical rules [1]. You can bolt behaviors and their types onto structures at will. This is how OO should be.

      I put together a visual argument on another thread on HN a few weeks ago:

      https://imgur.com/a/class-inheritance-vs-traits-oop-isnt-bad...

      [1] Though if you want rules on bounds and associated types, you can have them.

      • DaiPlusPlus 3 days ago

        > "Trait/Typeclass"-style compositional inheritance as in Rust and Haskell is sublime. It's similar to Java interfaces in terms of flexibility, and it doesn't enforce hierarchical rules.

        Yes-and-no.

        Interfaces still participate in inheritance hierarchies (`interface Bar extends Foo`), and that's in a way that prohibits removing/subtracting type members (so interfaces are not in any way a substitute for mixins). Composition (of interfaces) can be used instead of `extends`, but then you lose guarantees of reference-identity - oh, and only reference-types can implement interfaces which makes interfaces impractical for scalars and unusable in a zero-heap-alloc program.

        Interface-types can only expose virtual members: no public fields - which seems silly to me because a vtable-like mechanism could be used to allow raw pointer access to fields via interfaces, but I digress: so many of these limitations (or unneeded functionality) are consequences of the JVM/CLR's design decisions which won't change in my lifetime.

        Rust-style traits are an overall improvement, yes - but (as far as my limited Rust experience tells me) there's no succinct way to tell the compiler to delegate the implementation of a trait to some composed type: I found myself needing to write an unexpectedly large amount of forwarding methods by hand (so I hope that Rust is better than this and that I was just doing Rust the-completely-wrong-way).

        Also, oblig: https://boxbase.org/entries/2020/aug/3/case-against-oop/

        • int_19h 3 days ago

          How are interfaces with ability to provide default implementations for members (which both C# and Java allow today) not a substitute for mixins?

          "Only reference types can implement interfaces" is simply not true in C#. Not only can structs implement them, but they can also be used through the interface without boxing (via generics).

          • DaiPlusPlus 3 days ago

            > How are interfaces with ability to provide default implementations for members (which both C# and Java allow today) not a substitute for mixins?

            Those default-implementations are only accessible when the object is accessed via that interface; i.e. they aren't accessible as members on the object itself. Furthermore, interfaces (still) only declare (and optionally define) vtable members (i.e. only methods, properties, and events - which are all fundamentally just methods), not fields or any kind of non-static state, whereas IMO mixins should have no limitations and should behave the same as though you copied-and-pasted raw code.

            • int_19h 2 days ago

              > Those default-implementations are only accessible when the object is accessed via that interface; i.e. they aren't accessible as members on the object itself.

              That's true in C# but not in Java, so it's not something intrinsic to the notion of an interface.

              > Furthermore, interfaces (still) only declare (and optionally define) vtable members (i.e. only methods, properties, and events - which are all fundamentally just methods), not fields or any kind of non-static state

              This is true, but IMO largely irrelevant because get/set accessors are a "good enough" substitute for a field. That there is even a distinction between fields and properties in the first place is a language-specific thing; it doesn't exist in e.g. Eiffel.

              • RHSeeger 2 days ago

                Making the class add 2 methods and a field for every place the interface would define a field adds noise. And if you have a class with 20 interfaces, that can be a lot of noise. When you consider that the class itself doesn't actually need to know anything about the field because only the interface uses them, it's just... ugly.

                • int_19h 2 days ago

                  That's more of an issue with Java not having properties as first class language feature. In C#, you don't have to deal with fields at all, because auto-properties do all the same things:

                     int Foo { get; set; }
          • AstralStorm 3 days ago

            They just cannot access any field in the class itself. Which means... the default implementations fail to actually implement anything but the simplest methods or you need to expose API-private things via getters violating open-closed principle.

            (If you merge multiple interfaces, the implementations of the methods have to match. You end up with even more special getters for each one sometimes.)

            • int_19h 3 days ago

              In C# interface members can be implemented explicitly, which means that: 1) they are not visible on the object itself, only on interface-typed references to it, and 2) implementations of methods from multiple interfaces don't need to match since they live in separate namespaces.

              It's true that you can't access private members (not just fields) on `this` from the mixin interface. But explicit implementations of members mean that only someone explicitly downcasting the object will get access to those members, so accidental access is not an issue.

      • zozbot234 3 days ago

        Rust actually allows one to express "family tree" object inheritance quite cleanly via the generic typestate pattern. It isn't "garbage", it totally has its uses. It is however quite antithetical to modularity: the "inheritance hierarchy" can only really be understood as a unit, and "extensibility" for such a hierarchy is not really well defined. Hence why in practice it mostly gets used in cases where the improved static checking made possible by the "typestate" pattern can be helpful, which has remarkably little to do with "OOP" design as generally understood.

  • dataflow 3 days ago

    > And inheritance naturally suggests grouping interfaces into a tree in the way that seems of little value because in practice a tree probably doesn't represent the fundamental truth of things.

    "This doesn't represent the fundamental truth" does not imply "this has little value". Your navigation software likely doesn't account for cars passing each other on the road either -- or probably red lights for that matter -- and yet it's still pretty damn useful. The sweet spot is problem- and model-dependent.

  • starspangled 2 days ago

    I'm not sold on the evidence of much in the way of programming language features from the "object oriented" era.

    They were pushed by cultish types with little evidence. There was this assertion that all these things were wonderful and would reduce effort and therefore they must be good and we all must use them. We got object oriented everything including object oriented CPUs, object oriented relational databases, object oriented "xtUML". If you weren't object oriented you were a pile of garbage in those days.

    For all that, I don't know if there was ever any good evidence at all that any of it worked. It was like the entire industry all fell for snakeoil salesmen and are collectively too embarrassed about it to have much introspection or talk about it. Not that it was the last time the industry has fallen for snakeoil...

    • roguecoder 2 days ago

      If encapsulation wasn't useful, we wouldn't write microservices.

      If abstraction wasn't useful, we wouldn't use containers.

      • starspangled 2 days ago

        That's not evidence though even if we take it as true. You can of course make layers of abstraction or encapsulation without "object oriented" languages.

  • Retric 2 days ago

    Inheritance was oversold, but it can help remove a lot of boilerplate code. Early windows notoriously had hundreds of lines of code for a hello world program. Setting your own defaults and getting on with your day is great for dealing with a less refined API etc.

    Complex inheritance trees can make sense in niche application for similar reasons.

  • codr7 3 days ago

    But they're not building trees, that's how inheritance is mostly used today.

    After reading this, I'm thinking that intrusive lists is the one use of inheritance in C++ that makes any sense.

    • josephg 3 days ago

      I'd still generally prefer intrusive lists to be done via composition. I've seen plenty of intrusive lists where each item was a member of multiple lists at the same time - which is quite hard to do if you need to inherit from an intrusive list element superclass.

      • codr7 2 days ago

        metoo, but how do you pull that off in C++? How do you get back from node to containing value?

        Multiple inheritance, possible but you'd have to jump some hoops to disambiguate since you're dealing with multiple copies of the same base class.

        • josephg 2 days ago

          Composition. Sorry about the syntax - its been awhile since I wrote C++.

              class<T> IntrusiveListNode { T* next, T* prev }
          
              class SomeObj {
                  IntrusiveListNode<SomeObj> list_foo;
                  IntrusiveListNode<SomeObj> list_bar;
              }
          • codr7 2 days ago

            Ah, of course; templates and linking the full object.

            Thanks for the update, my C++ is pretty rusty.

            But it brings up another problem for me; when iterating a list like that, how do you know which of the links to follow? I get that you can do it manually step by step, but if you wanted to say write an iterator. Member pointers?

  • jmull 2 days ago

    So... what even is the purpose of computer language abstractions?

    To provide building blocks useful for the construction of programs.

    There's a number of properties that are good for such building blocks... composability, flexibility, simplicity, comprehensibility, etc.

    Naturally, these properties can conflict, so the goal would be to provide a minimal set of interoperable building blocks providing good coverage of the desirable properties, to allow the developer can choose the appropriate one for a give circumstance and to change when needed. E.g., they could choose to use a simple but less flexible block in one situation, or a more complicated or less performant block in another.

    IMO, inheritance is a decent building block -- simple and easy to understand, though with somewhat limited applicability.

    We can imagine improvements (particularly to implementation) but I think it got a bad rep mostly due to people not understanding its uses and limitations.

    ...I've got to say, though, if you aren't figuring out how to use the simple and easy tools, you're really not going to do better with more complicated and capable tools. People hate to admit it, but the best of us are still highly confused monkeys haphazardly banging away at keyboards, barely able to hold a few concepts in our heads at one time. Simple is good for us.

  • strogonoff 2 days ago

    Like any map, the inheritance pattern is bad, except when it works. It’s a strategic capability to be able to guess well which is which in given context.

    My first foray into serious programming was by way of Django, which made a choice of representing content structure as classes in the codebase. It underwent the usual evolution of supporting inheritance, then mixins, etc. Today I’d probably have mixed feelings about conflating software architecture with subject domain so blatantly: of course it could never represent the fundamental truth. However, I also know that 1) fundamental truth is not losslessly representable anyway (the map cannot be the territory), 2) the only software that is perfectly isolated from imperfections of real world is software that is useless, and 3) Django was easy to understand, easy to build with, and effectively fit the purpose.

    Any map (requirement, spec, abstraction, pattern) is both a blessing that allows software to be useful over longer time, and a curse that leads to its obsolescence. A good one is better at the former than the latter.

  • mexicocitinluez 2 days ago

    > And inheritance naturally suggests grouping interfaces into a tree in the way that seems of little value because in practice a tree probably doesn't represent the fundamental truth of things.

    The fundamental truth of things? What are you even talking about? What fundamental truth of things? And what does that have anything to do with building software?

  • tippytippytango 3 days ago

    If you pretend/imagine it was intentional, and insightful, you've created a nerd trap for amateur ontologists. Some of which decide to become professional ontologists and sell books on objected oriented design.

    • tobr 3 days ago

      Your lovely typo there makes me realize how often I’ve had to deal with objection-oriented programming.

  • ninetyninenine 3 days ago

    > a tree probably doesn't represent the fundamental truth of things

    It does. Trees appear in nature all the time. It's the basis of human society, evolution and many things.

    Most of programming moves towards practicality rather then fundamental truth. That's why you get languages like golang which are ugly but practical.

    • josephg 2 days ago

      Botanical trees appearing in nature don't make them "the fundamental truth of things". And in what way are trees the basis of human society? Thats such a strange claim. Are you talking about family trees? Because they're actually directed acyclic graphs.

      Even if you want to claim that trees are a common data structure, that doesn't mean they're appropriate in any specific case. Should we therefore arrange all websites in a tree? Should relational databases be converted to trees, because "they're the basis of human society"? What tosh.

      Programming moves toward practicality because software is created to do work. Taxonomies are entirely and completely worthless. The classic Animal -> Mammal -> Cat example for inheritance is a fascinating ontology, and an entirely worthless piece of software.

      • ninetyninenine 2 days ago

        fundamental truth of things is probably the wrong word choice.

        It's more like there are many fundamental concepts and trees are one such concept. I don't think there is a singular fundamental truth of things.

        >Even if you want to claim that trees are a common data structure, that doesn't mean they're appropriate in any specific case. Should we therefore arrange all websites in a tree? Should relational databases be converted to trees, because "they're the basis of human society"? What tosh.

        I never made this claim though?

        >Programming moves toward practicality because software is created to do work. Taxonomies are entirely and completely worthless. The classic Animal -> Mammal -> Cat example for inheritance is a fascinating ontology, and an entirely worthless piece of software.

        I mentioned this because parent poster is talking about fundamental truths. I'm saying trees are fundamental... But they may not be practical.

      • inglor_cz 2 days ago

        "Taxonomies are entirely and completely worthless."

        Hard disagree. Knowing that AES and Twofish are block ciphers is useful when dealing with cryptography. Many categories of algorithms and objects are naturally taxonomic.

        Even HTML+CSS has (messy) inheritance.

        • josephg 2 days ago

          I may have overstated the claim in that sentence. More accurately, I think taxonomies aren't, themselves, useful. If they don't help you solve problems, you're just stamp collecting. I've seen that happen way too many times - otherwise smart people thinking they're being productive by writing a whooole lot of useless classes, where a few functions and some if statements would do it.

          But don't get me wrong - I'm totally in favour of having common interfaces. They aren't great because they form a taxonomy. They're great because it helps us abstract. Whether its Iterator or different software implementing HTTP, interfaces actually help us solve actual problems.

          > Knowing that AES and Twofish are block ciphers is useful when dealing with cryptography.

          The useful fact in this example is that they both do a similar thing. They form a type class from their common behaviour. Behaviour they may share with - for example - a compression algorithm. Thats what actually matters here.

        • tialaramex 2 days ago

          I think this is a lumpers/ splitters thing. The problem with a tree structure is that it assumes a very particular shape and you have to distort your understanding of the world to make that fit reality which has no such inherent pattern.

          So, the taxonomy isn't useless, but, it's also never sufficient.

  • ninetyninenine 3 days ago

    It is a good idea because it's the most fundamental idea.

    You have two objects. A and B. How do you merge the two objects? A + B?

    The most straight forward way is inheritance. The idea is fundamental.

    The reason why it's not practical has more to do with human nature and the limitations of our capabilities in handling complexity then it has to do with the concept of inheritance itself.

    Literally think about it. How else do you merge two structs if not using inheritance?

    The idea that inheritance is not fundamental and is wrong in nature is in itself mistaken.

    • astrobe_ 2 days ago

      I found myself in a situation where I had to reinvent data structures from scratch in an assembly-like language: "properties" or "fields" are just offsets relative to a pointer (runtime) or an address (compile-time).

      The need to extend a data structure to add more fields comes almost immediately. Think: something like the C "hack" of embedding the "base" structure as the first field of the "derived" structure:

         struct t_derived
         {
             struct t_base base;
             int extra_data;
         };
      
      Then you can pass a derived_t instead of base_t with some type casting and caveats. This is "legal" in C because the standard guarantees that base has offset 0.

      Of course our "extra_data" could be a structure, but although it would look like "A+B" it is actually a concatenation.

    • dgb23 2 days ago

      > How else do you merge two structs if not using inheritance?

      By merging them. Structs are product types. If you merge them, you get a bigger product type. You don't need inheritance (ADTs) for that.

      The more useful point of inheritance is having shared commonality. But modern languages make it convenient to express that without using ADTs/inheritance.

      TypeScript is fully structurally typed. If you combine a Foo and a Bar it is something new, but keeps being both a Foo and a Bar as well.

      Go is structurally typed to a relatively high degree as well. You can embed types (including structs) into structs and only care about the individual parts in your functions. And you have composable and implicit interfaces.

      Clojure has protocols and generally only cares about the things you use or define to use in functions. It allows you to do hierarchical keyword ontologies if you want, but I see it rarely used.

      These languages and many others favor two fundamental building blocks: composition and signatures. The latter being either about data fields or function signatures. The neat part is these aren't entangled: You can use and talk about them separately.

      How fundamental is inheritance if it can be fully replaced by simpler building blocks?

      • ninetyninenine 2 days ago

        >By merging them. Structs are product types. If you merge them, you get a bigger product type. You don't need inheritance (ADTs) for that.

        Merging structs and inheritance are fundamentally the same thing.

        >How fundamental is inheritance if it can be fully replaced by simpler building blocks?

        It can't be replaced. Combining Foo and Bar in the way you're thinking involves additional primitives and concepts like nesting. If Foo and Bar share a same property the most straight forward way of handling is overriding one property with the other. Overriding IS inheritance.

        We aren't dealing with product types in the purest form either. These product types have named properties and you need additional rules to handle conflicting names.

        In fact once you have named properties the resulting algebra from multiplying structs is not consistent with the concept of multiplication whether you use inheritance or "object composition"

    • josephg 3 days ago

      > Literally think about it. How else do you merge two structs if not using inheritance?

      What? Using multiple inheritence? That's one of the worst ideas I've ever seen in all of computer science. You can't just glue two arbitrary classes together and expect their invariants to somehow hold true. Even if they do, what happens when both classes implement a method or field with the same name? Bugs. You get bugs.

      I've been programming for 30 years and I've still never seen an example of multiple inheritance that hasn't eventually become a source of regret.

      The way to merge two structs is via composition:

          struct C {
              a: A,
              b: B,
          }
      
      If you want to expose methods from A or B, either wrap the methods or make the a or b fields public / protected and let callers call c.a.foo().

      Don't take my word for it, here's google's C++ style guide[1]

      > Composition is often more appropriate than inheritance.

      > Multiple inheritance is especially problematic, because it often imposes a higher performance overhead (in fact, the performance drop from single inheritance to multiple inheritance can often be greater than the performance drop from ordinary to virtual dispatch), and because it risks leading to "diamond" inheritance patterns, which are prone to ambiguity, confusion, and outright bugs.

      > Multiple inheritance is permitted, but multiple implementation inheritance is strongly discouraged.

      [1] https://google.github.io/styleguide/cppguide.html#Inheritanc...

      • RHSeeger 2 days ago

        > Even if they do, what happens when both classes implement a method or field with the same name?

        It's done in Java with interfaces with default implementations, and the world hasn't imploded. It just doesn't seem like that big of a problem.

        • josephg 2 days ago

          > It just doesn't seem like that big of a problem.

          It really, really depends on the codebase. There are absolute mammoth tire fire codebases out there - particularly in "enterprise code". These are often made up of insane hierarchies of classes which in practice do nothing but obscure where any of the actual logic lives for your program. AbstractFactoryBuilderImpl. Wild goose chases where you need some bizzaire and fragile incantation to actually create an object, because every class somehow references every other class. And you can't instantiate anything without instantiating everything else first.

          If you haven't seen it in your career yet (or at all), you are lucky. But I promise you, hell is programmed by mediocre teams working in java.

          • RHSeeger 2 days ago

            I've seen such difficult to follow code in Java, and I complain about it every time. But having multiple inheritance isn't the cause (because Java doesn't have that). Nor does having multiple interfaces with default implementations have any real impact (at least in _any_ case I've seen).

            The only real difference I see between multiple inheritance and multiple interfaces with default implementations is constructors. And they can be handled in the same way as default implementations; requiring specific usage/ordering.

      • iExploder 3 days ago

        I guess the point was never how to do thing properly, but:

        "how to join two struts with least amount of work and thinking so my manager can tick off a box in excel"

        in such case inheritance is a nice temporary crutch

      • ninetyninenine 3 days ago

        >What? Using multiple inheritence?

        You just threw this in out of nowhere. I didn't mention anything about "multiple" inheritance. Just inheritance which by default people usually mean single inheritance.

        That being said multiple inheritance is equivalent to single inheritance of 3 objects. The only problem is because two objects are on the same level it's hard to know which property overrides which. With a single chain of inheritance the parent always overrides the child. But with two parents, we don't know which parent overrides which parent. That's it. But assume there are 3 objects with distinct properties.

           A -> B -> C
        
        would be equivalent to

           A -> C <- B. 
        
        They are isomorphic. Merging distinct objects with distinct properties is commutative which makes inheritance of distinct objects commutative.

           C -> B -> A == A -> B -> C
        
        >I've been programming for 30 years and I've still never seen an example of multiple inheritance that hasn't eventually become a source of regret.

        Don't ever tell me that programming for 30 years is a reason for being correct. It's not. In fact you can be doing it for 30 years and be completely and utterly wrong. Then the 30 years of experience is more of a marker of your intelligence.

        The point is YOU are NOT understanding WHAT i am saying. Read what I wrote. The problem with inheritance has to do with human capability. We can't handle the complexity that arises from using it extensively.

        But fundamentally there's no OTHER SIMPLER way to merge two objects without resorting to complex nesting.

        Think about it. You have two classes A and B and both classes have 90% of their properties shared. What is the most fundamental way of minimizing code reuse? Inheritance. That's it.

        Say you have two structs. The structs contain redundant properties. HOW do you define one struct in terms of the other? There's no simpler way then inheritance.

        >> Composition is often more appropriate than inheritance.

        You can use composition but that's literally the same thing but wierder, where instead of identical properties overriding other properties you duplicate the properties via nesting.

        So inheritance

           A = {a, b}, C = {a1}, A -> C = {a1, b}
        
        Composition:

           A = {a, b}, C = {a1}, C(A) = {a1, {a, b}}
        
        That's it. It's just two arbitrary rules for merging data.

        If you have been programming for 30 years you tell me how to fit this requirement with the most minimal code:

        given this:

           A = {a, b, c, d}
        
        I want to create this:

           B = {a, b, c, d, e} 
        
        But I don't want to rewrite a, b, c, d multiple times. What's the best way to define B while reusing code? Inheritance.

        Like I said the problem with inheritance is not the concept itself. It is human nature or our incapability of DEALING with the complexity that arises from it. The issue is the coupling is two tight so you make changes in one place it creates an unexpected change in another place. Our brains cannot handle the complexity. The idea itself is fundamental not stupid. It's the human brain that is too stupid to handle the emergent complexity.

        Also I don't give two flying shits about google style guides after the fiasco with golang error handling. They could've done a better job.

        • josephg 2 days ago

          > But assume there are 3 objects with distinct properties...

          Why would we assume that? If the objects are entirely distinct, why are you combining them together into one class at all? That doesn't make any sense. Let distinct types be distinct. Let consumers of those types combine them however they like.

          > But fundamentally there's no OTHER SIMPLER way to merge two objects without resorting to complex nesting. [...] You have two classes A and B and both classes have 90% of their properties shared. What is the most fundamental way of minimizing code reuse? Inheritance. That's it.

          So now the objects have 90% of their properties shared. That's different from what you were saying earlier. But moving on...

          The composition answer is similar to the inheritance answer. Take the common parts and extract them out into a self contained type. Use that type everywhere you need it - eg by including it in both A and B.

          > given this: A = {a, b, c, d} I want to create this: B = {a, b, c, d, e}. [...] What's the best way to define B while reusing code?

          Via composition, you do it like this:

              B = { a: A, e }
          
          What could be simpler than that?

          You keep claiming that inheritance is simpler. But then you go on to say this:

          > The problem with inheritance has to do with human capability. We can't handle the complexity that arises from using it extensively. [..] It is human nature or our incapability of DEALING with the complexity that arises from it. The issue is the coupling is two tight so you make changes in one place it creates an unexpected change in another place. Our brains cannot handle the complexity.

          In other words, using inheritance increases the complexity of the resulting code for the human brain. It makes our code harder to read & understand. I absolutely agree with this criticism you're making.

          And that's an incredibly damning criticism, because complexity is an absolute killer. Your capacity to wrangle the complexity of a given piece of code is the single greatest limitation of any software developer, no matter how skilled. Any change that makes your code more complex and harder to understand must be worth it in some way. It must pay dividends. Using inheritance brings no benefit in most cases over the equivalent compositional code. The only thing that happens is that - as you say - it makes the software harder for our brains to handle.

          If you ask me, that's a terrible decision to make.

          • ninetyninenine 2 days ago

            >Why would we assume that? If the objects are entirely distinct, why are you combining them together into one class at all? That doesn't make any sense. Let distinct types be distinct. Let consumers of those types combine them however they like.

            Human = torso, legs, arms. Three distinct objects combine into one thing. A human by definition is the union of these things. It's fundamental. It's just your bias is trying to see it as something else.

            >So now the objects have 90% of their properties shared. That's different from what you were saying earlier. But moving on...

            So? I can talk about multiple things right? This is allowed in life right? Did i break the law here?

            >The composition answer is similar to the inheritance answer. Take the common parts and extract them out into a self contained type. Use that type everywhere you need it - eg by including it in both A and B.

            Composition is the same thing. But it's saying instead of overriding duplicate properties, Clone the duplicate property. That's it. And it uses nesting to achieve this. This arbitrary rule isn't more fundamental then overriding the duplicate property.

            >Via composition, you do it like this:

            Why don't you take a look at my examples again. You are either not able to comprehend or you didn't read it. I literally said the same thing:

               B = {a: A, e}
            
            The above a complete dupe of what I wrote.

               B = {a, b, c , d, e}
            
            Just nested. Which I brought up:

               B = {{a, b, c, d}, e}
            
            Is it not? Please read my response replying with stuff like this. Read it thoroughly.

            >And that's an incredibly damning criticism, because complexity is an absolute killer.

            Not exactly it's not that straightforward. Because inheritance minimizes code copying. It reuses code in the most efficient way possible. So actually lines of code and duplicate code actually goes down. So complexity in one area falls and rises in another area.

            Our brains are biased towards handling complexity of duplicate code better then tightly coupled code.

            • josephg 2 days ago

              > Human = torso, legs, arms. Three distinct objects combine into one thing. A human by definition is the union of these things. It's fundamental. It's just your bias is trying to see it as something else.

              No, it’s not. If I put a torso, legs and arms (and perhaps a head) on a table, I don’t get a human being. I’d say a human composes all of those things (and more!). But a human doesn’t inherit from them. For example, each leg can kick(). But you can’t inherit from two legs! And if you did, which leg is the one that kicks when you call the function? Much better to have human class which contains two legs. Then human can have a kick(RIGHT_LEG) function which delegates its behaviour to right_leg.kick().

              There’s lots more ways composition helps us model this. Let’s say we want to model blood temperature, which is tracked in every limb separately. Composition makes it more straightforward to have different behaviour (& state) in the Body class for blood temperature than in any of the limbs. (Eg maybe the blood temperature is the average of all limbs temperature. That is more straightforward to implement with composition.)

              > inheritance minimizes code copying. It reuses code in the most efficient way possible.

              On the surface, I agree with this claim. But it’s funny - programs which make heavy use of OO always seem unnecessary verbose. I wonder why that is? I’m thinking of Java where it’s common to see utility classes that just have 1 or 2 fields take up 100+ lines of code, due to class boilerplate, custom hashCode, toString and isEqual methods and all the rest.

              But in any case, as you say, we aren’t just trying to optimise for the fewest lines of code. We’re trying to optimize for how easy something is to read, write and maintain. Adding code to increase simplicity is often worth it. Inheritance increases complexity because it adds a layer of hidden control flow. When I’m reading a program, I need to do a lot of work to figure out if foo.bar() is calling a function in one of the base classes or in the derived class. As you say, humans don’t deal with that kind of complexity well. In general, explicit is better than implicit - this.leg.kick() is more explicit than this.kick() when kick() exists somewhere in one (or more?) of the base classes.

              Also let’s say I have 3 classes A, B extends A and C extends A. If there’s a bug in B that involves something in the base class A, fixing that bug may break implicit invariants in C. This kind of “spooky action at a distance” is horrible. Ironically, it violates the principle of encapsulation that OO claims as one of its core principles. I find this kind of problem is rarer in compositional systems. And when it happens, it’s usually much more straightforward to debug and fix. The reason is because classes are all self contained. You don’t have partially-specified base classes that only kinda sorta maintain their invariants. And derived classes don’t implicitly include their base class’s behaviour. As a result, there’s less implicit entanglement. B and C can much more easily change how they wrap A’s behaviour.

              At the end of the day, I think we more or less agree that inheritance makes code harder to reason about. I don’t write code to express a pure conceptual representation of the world. (And neither should you!). I write code to get stuff done. If inheritance makes it harder for humans to be productive with our software, then that’s reason enough to abandon it.

              • ninetyninenine 2 days ago

                I’m not gonna read the full thing. I’m just responding to the first part. Composes or inheritance is just vocabulary for two things that are the same with slight differences.

                Composes duplicates identical properties via nesting.

                Inheritance overrides properties that are identical.

                That’s it. I’m done.

                • josephg 2 days ago

                  Maybe that's our ultimate disagreement. I think the difference between composition and inheritance matters a lot. It changes how we break our software into modules. It sounds like you think of inheritance as being fundamentally the same as composition "with slight differences". Although even you admit that "Our brains cannot handle the complexity [of inheritance]". If that's true (and I think it is), the difference is surely more than skin deep.

                  I agree about nesting. But nesting matters, because it forces us to design components which make sense in isolation. As a result, composition encourages - and in many ways requires - better modularity in code. Inheritance does not. Base classes are often poorly conceived, poorly specified grab-bags of state and functions. They lead to hard to understand, hard to follow code.

                  Earlier in this thread you insulted my intelligence. You said this:

                  > Don't ever tell me that programming for 30 years is a reason for being correct. It's not. In fact you can be doing it for 30 years and be completely and utterly wrong. Then the 30 years of experience is more of a marker of your intelligence.

                  I'm curious if you'll still back the argument you've made here after you've been programming for 30 years too. You're clearly already suspicious of how and why inheritance makes code harder to understand. I suspect in a few years, you'll come around to my point of view on this. But I'd love to know if I'm wrong.

                  • ninetyninenine 2 days ago

                    >I'm curious if you'll still back the argument you've made here after you've been programming for 30 years too. You're clearly already suspicious of how and why inheritance makes code harder to understand. I suspect in a few years, you'll come around to my point of view on this. But I'd love to know if I'm wrong.

                    I did insult your intelligence. Because when you said you have 30 years of experience I hear total arrogance. It's like "I'm right and you're wrong because I have 30 years of experience" When I hear that I just want the other person to shut the hell up.

                    >I agree about nesting. But nesting matters, because it forces us to design components which make sense in isolation. As a result, composition encourages - and in many ways requires - better modularity in code. Inheritance does not. Base classes are often poorly conceived, poorly specified grab-bags of state and functions. They lead to hard to understand, hard to follow code.

                    Again, you resuse the same code if you don't use inheritance. A cat walks, so does an animal, do does a dog. You have to write walk() twice if you don't use inheritance. There's a trade off here.

                    The difference is skin deep. It's the emergent complexity that is NOT skin deep.

                    combining objects via "object composition" or "inheritance" is different. One way is not more right then the other. It's simply that you can't handle the hierarchical relationships.

                    But think about it. If you have a deeply nested Object where you don't use inheritance. Then all the objects have multitudes of redundant properties, doesn't that result in complex code as well? And how does nesting objects make it less complex then inheriting objects? It's more of code navigation problem in the sense that when you use inheritance and you look at a child derived from generations of inheritance it's just hard to read and figure out what the final object is.

                    With object composition the view is the same. You have an object that holds generations of nested objects. The difference is you can control click and follow the definition of the nested object so it's more visible.

                    Thus it seems to me the issue with the complexity is that inheritance simply does not give you a widget you can control click into easily to follow the definition. This whole problem could be characterized by a user interface issue because it's not evident to me how an object with nested objects 1000000 layers deep is more complex then the same object derived from 100000 ancestors.

                    • josephg 2 days ago

                      > The difference is skin deep. It's the emergent complexity that is NOT skin deep.

                      Yeah I agree with this. There's something about inheritance is more than skin deep. Something which changes how we conceive of our software. I agree that whatever that is, its quite important and impactful.

                      I could talk for days on what I think that difference is. I wrote a whole bit, but deleted it because I think I've said enough about what I think.

                      What do you think the difference is? How is it possible for composition and inheritance to be so different, if they're so alike on the surface?

                      • ninetyninenine a day ago

                        I literally wrote it above and you didn’t read. It’s a user interface issue, Let me copy it here:

                        But think about it. If you have a deeply nested Object where you don't use inheritance. Then all the objects have multitudes of redundant properties, doesn't that result in complex code as well? And how does nesting objects make it less complex then inheriting objects? It's more of code navigation problem in the sense that when you use inheritance and you look at a child derived from generations of inheritance it's just hard to read and figure out what the final object is because there’s no easy way to visualize or follow the derived properties.

                        With object composition the view is the same. You have an object that holds generations of nested objects. The difference is you can control click and follow the definition of the nested object so it's more visible.

                        Thus it seems to me the issue with the complexity is that inheritance simply does not give you a widget you can control click into easily to follow the definition and see what the derived methods are.

                        This whole problem is characterized by a user interface issue because it's not evident to me how an object with nested objects 1000000 layers deep is more complex then the same object derived from 100000 ancestors.

                        Think about the emergent complexity here. An object derived from inheriting a chain of 1000 inherited objects is actually less complex then that same chain of objects created via composition. Because duplicate properties don’t get overridden you have more data stored here then inheritance. It’s actually more complex.

                        The problem is in the user interface.

                        Create an IDE that automatically fills out all the derived methods of an object and allows you to control click to the ancestor where the derived method comes from and the issue seems to me to be solved.

        • matkoniecz 2 days ago

          > Say you have two structs. The structs contain redundant properties. HOW do you define one struct in terms of the other? There's no simpler way then inheritance.

          why inheritance would make it easier?

          • ninetyninenine 2 days ago

            I have: A = {a, b, c, d, e}

            I want to write: B = {a, b, c, d, e, f, g}

            But I don't want to write duplicate code

            So I write:

               B = B(A) = {A, e, f, g} 
            
            aka I use inheritance.

            Are there easier ways? No. Inheritance is the most fundamental way of doing this. Composition is just a work around as it results in arbitrary nesting. But ultimately it's the same thing too.

        • ab5tract 2 days ago

          The real major issue with multiple inheritance is that only a few languages (Eiffel, for one) seem to require declaring invariants for each class.

          Then multiple inheritance is much cleaner because you can test constructor arguments against the union of these invariants before even hitting a constructor.

          Note that I’ve never used it, but it did strike me that this was “the” way to include multiple inheritance in a language. But for some reason (run time performance maybe?) no one else seems to have done it this way.

smallstepforman 2 days ago

There is this wonderful presentation by Herb Sutter talking about how the C++ concept “class” covers over 20 other abstractions, and that Bjarne’s choice for C++ was the right choice since it offers so much power and flexibility and expressive power in a concise abstraction.

Other languages (just like the article) only saw the downsides to such a generic abstraction that they added N times more abstractions (so split inheritance, interfaces, traits, etc) and rules for interactions that it significantly complicated the language with fundamentally no effective gains.

In summary, Herb will always do a better job than me explaining why the choices in the design of C++ classes, even with multiple inheritance, is one of the key factors of C++ success. With cppfront, he extends this idea with metaclasses to clearly describe intent. I think he is on the right track.

  • dgellow 2 days ago

    Could you share a link?

virgilp 2 days ago

I find that structural typing is the most useful thing and unfortunately few languages support it. I'd like a language where:

- If I have a class Foo and interface Bar, I should be easily able to pass a Foo where Bar is required, provided that Foo has all the methods that Bar has (sometimes I don't control Foo and can't add the "implements Bar" in it).

- I can declare "class Foo implements Bar", but that only means "give me a compilation error if Bar has a method that Foo doesn't implement" - it is NOT required in order to be able to pass a Foo object to a method that takes a Bar parameter

- Conversely, I should be able to also declare "interface Foo implementedBy Baz" and get a compilation error if either one of them is modified in a way that makes them incompatible (again - this does not mean that Baz is the _only_ implementor, just that it's one of them)

- Especially with immutable values - the same should apply to data. record A extends B, C only means "please verify that A has all the members that B & C have, and as such whenever a B record is required, I can pass an A instead". I should be able to do the reverse too (record B extendedBy A). Notably, this doesn't mean "silently import members from B, and create a multiple-inheritance-mess like C++ does".

(I do understand that there'd be some performance implications, but especially with a JIT a feel these could be solved; and we live in a world where I think a lot of code cares more about expressiveness/ understandability than raw performance)

  • phiresky 2 days ago

    TypeScript does supports all of these - `C implements I` is not necessary but gives compile errors if not fulfilled.

    You can use `o satisfies T` wherever you want to ensure that any object/instance o implements T structurally.

    To verify a type implements/extends another type from any third-party context (as your third point), you could use `(null! as T1) satisfies T2;`, though usually you'd find a more idiomatic way depending on the context.

    Of course it's all type-level - if you are getting untrusted data you'll need a library for verification. And the immutable story in TS (readonly modifier) is not amazing.

    • roguecoder 2 days ago

      The problem with TypeScript is that it is ridiculously verbose. It is everything people used to complain Java was back in the 90s.

  • ncruces 2 days ago

    The first 3 are provided by Go, basically.

  • roguecoder 2 days ago

    I want the same things you do, and the closest I've found is writing Ruby with RDocs on all the public methods of a class.

cturner 3 days ago

We use the word inheritance to refer to two concepts. There is implementation-inheritance. There is type-inheritance. These ideas are easily confused, which should be cause to have distinct words for them. Yet we don't. (Although Java does, effectively)

chuckadams 2 days ago

I think a lot of the arguments against inheritance come from C++'s peculiar implementation of it, which it clearly, ah, inherited from Simula. Slicing, ambiguous diamond inheritance, stuff like that are C++ problems, not inheritance problems. This isn't to say inheritance isn't problematic, but when you're making a properly substitutable sub-type of something, it's hard to beat.

  • joe_the_user 2 days ago

    What are examples of better inheritance?

    • chuckadams 2 days ago

      For the specific problems I mentioned like slicing, literally anything else that actually abstracts the memory layout.

      For OOP in general, I'd say anything with a metaobject protocol for starters, like Smalltalk, Lisp (via CLOS), Python, Perl (via Moose). All but the first support multiple inheritance, but also have well-defined method resolution orders. Multiple inheritance might still lead frequently to nasty spaghetti code even in those languages, but it will still be predictable.

      CLOS and Dylan have multiple dispatch, which is just all kinds of awesome, but alas is destined to remain forever niche.

rakejake 3 days ago

IMHO Inheritance (especially the C++ flavored inheritance with its access specifiers and myriad rules) has always scared me. It makes a codebase confusing and hard to reason with. I feel the eschewing of inheritance by languages such as Go and Rust is a step in the right direction.

As an aside, I have noticed that the robotics frameworks (ROS and ROS2) heavily rely on inheritance and some co-dependent C++ features like virtual destructors (to call the derived class's destructor through a base class pointer). I was once invited to an interview for a robotics company due to my "C++ experience"and grilled on this pattern of C++ that I was completely unfamiliar with. I seriously considered removing C++ from my resume that day.

  • masfoobar 3 days ago

    To me, inheritence makes sense if you view your codebase as actual "Objects"

    The reality is that a codebase is not that simple. Many things you create are not representable as realworld "objects" - to me, this is where is gets confusing to follow especially when the code gets bigger.

    I remember those OOP books (I cannot comment on modern OOP books) where the first few chaptors would use Shapes as an example. Where A Circle, Square, Triangle, etc.. would inherit the Shape object. Sure, in simple examples like this.. it makes sense.

    I remember covering inheritence and how to tell if its better or composition... which is the "Object IS X" or "Object HAS X" - so you base you're heirarchy around that mindset.

    - "A Chair is Furniture" (Chair inherits Furniture) - "A Chair has Legs" (Chair has array of Leg)

    I will always remember my first job - creating shop floor diagrams where you get to select a Shelf or Rack and see the visual representation of goods, etc. My early codebase was OOP... a Product, Merchandise, Shelf, Bay, Pegboard, etc. Each object inherits something in one way or another. Keeping on top of it eventually became a pain. I think there was, overall, about 5 levels of inheritence.

    I reviewed my codebase one day and decided to screw it -- I would experiment other approaches. I ended up created simple classes with no inheritence. Each class was isolated from one another with the exception of a special Id which represented "something" like a Pin, or Shelf, etc. Now my code was flexible... "A Shelf has this and this"

    In later years I realised what I did was following along the lines of what is commonly known as ECS or Entity-Component-System. Seems popular in games (and I viewed that project is a game-like fashion so it makes sense)

    • tmountain 2 days ago

      I’m not on the cutting edge of gamedev, but I still believe that ECS is a solid pattern with lots of use cases.

      • ogogmad 2 days ago

        Sounds like relational databases: Entities are IDs. Components are tables with an ID column.

  • quietbritishjim 2 days ago

    To be fair, deleting a derived object through a base class pointer is pretty basic C++. Slicing and virtual destructors are usually the first couple of things you learn about after virtual methods and copy constructors/assignment.

impure 3 days ago

Huh, I was always told that inheritance hurt performance as it requires additional address lookups. Thats why many game engines are moving away from it.

I guess it could simplify the GC but modern garbage collectors have come a long way.

  • i_c_b 2 days ago

    I was in the game industry when we originally transitioned from C to C++, and here's my recollection of the conversations at the time, more or less.

    In C++, inheritance of data is efficient because the memory layout of base class members stays the same in different derived classes, so fields don't cost any more to access.

    And construction is (relatively fast, compared to alternatives) because setting a single vtable pointer is faster than filling in a bunch of variable fields.

    And non-virtual functions were fast because, again, static memory layouts and access and inlining.

    Virtual functions were a bit slower, but ultimately that just raised the larger question of when and where a codebase was using function pointers more broadly - virtual functions were just one way of corralling that issue.

    And the fact that there were idiomatic ways to use classes in C++ without dynamically allocating memory was crucial to selling game developers on the idea, too.

    So at least from my time when this was happening, the general sense was that, of all the ways OO could be implemented, C++ style OO seemed to be by far the most performant, for the concerns of game developers in the late 90's / early 2000's.

    I've been out of the industry for a while, so I haven't followed the subsequent conversations since too closely. But I do think, even when I was there, the actual reality of OO class hierarchies were starting to rear their ugly heads. Giant base classes are indeed drastically bad for caches, for example, because they do tend to produce giant, bloated data structures. And deep class hierarchies turn out to be highly sub-optimal, in a lot of cases, for information hiding and evolving code bases (especially for game code, which was one of my specialties). As a practical matter, as you evolve code, you don't get the benefits of information hiding that were advertised on the tin (hence the current boosting of composition over inheritance). I think you can better, smart discussions about those issues in this thread, so I won't cover them.

    But that was a snapshot of those early experiences - the specific ways C++ implemented inheritance for performance reasons were definitely, originally, much of the draw to game programmers.

  • andyferris 3 days ago

    As I understand it, back when Simula and LISP were invented it was generally the case that loads and stores took 1 cycle and there were no CPU caches. These pointer-chasing languages and techniques really weren't technically bad for the computers of the time - it's just that we have a larger relative penalty for randomly accessing our Random Access Memory these days so locallity is important (hence data-oriented design, ECS, etc).

    I am kind of amused they _removed_ first-class functions though!

    • int_19h 3 days ago

      Function arguments weren't actually first-class to begin with. In Algol 60 (of which Simula started as a superset), you could pass functions as arguments to other functions, but that's it - it wasn't a proper type so you couldn't return it, shove it into a variable, have an array of functions etc. Basically, it had just enough restrictions that you would never get up in a situation where you could possibly call a function for which the corresponding activation frame (i.e. locals) could be gone. But when Simula added classes and objects, now you could suddenly capture arguments in a way that allows them to outlive the callee.

  • kragen 3 days ago

    No, inheritance does not require additional address lookups. Single inheritance as discussed here doesn't even require additional address arithmetic; the address of the subclass instance is the same as the address of the superclass instance.

    Yes, current GCs are very fast and do not suffer from the problems Simula's GC suffered from. Nevertheless, they do still have an easier time when you embed record A as a field of record B (roughly what inheritance achieves in this case) rather than putting a pointer to record A in record B. Allocation may not be any faster, because in either case the compiler can bump the nursery pointer just once (with a copying collector). Deallocation is maybe slightly faster, because with a copying collector, deallocation cost is sort of proportional to how much space you allocate, and the total size of record B is smaller with record A embedded in it than the total size of record A plus record B with a pointer linking them. (That's one pointer bigger.) But tracing gets much faster when there are no pointers to trace.

    You will also notice from this example that it's failing to embed the superclass (or whatever) that requires an additional record lookup. And probably a cache miss, too.

    I think the reason many game engines are moving away from inheritance is that they're moving away from OO in general, and more generally the Lisp model of memory as a directed graph of objects linked by pointers, because although inheritance reduces the number of cache misses in OO code, it doesn't reduce them enough.

    I've written about this at greater length in http://canonical.org/~kragen/memory-models/, but I never really finished that essay.

    • josephg 3 days ago

      > No, inheritance does not require additional address lookups. Single inheritance as discussed here doesn't even require additional address arithmetic; the address of the subclass instance is the same as the address of the superclass instance.

      Yes it does! Inheritance itself is fine, but inheritance almost always means virtual functions - which can have a significant performance cost because of vtable lookups. Using virtual functions also prevents inlining - which can have a big performance cost in critical code.

      > Nevertheless, they do still have an easier time when you embed record A as a field of record B (roughly what inheritance achieves in this case) rather than putting a pointer to record A in record B.

      Huh? No - if you put A and B in separate allocations, you get worse performance. Both because of pointer chasing (which matters a great deal for performance). And also because you're putting more pressure on the allocator / garbage collector. The best way to combine A and B is via simple composition:

          struct C { a: A, b: B }
      
      In this case, there's a single allocation. (At least in languages with value types - like C, C++, C#, Rust, Swift, Zig, etc). In C++, the bytes in memory are actually identical to the case where B inherits from A. But you don't get any class entanglement, or any of the bugs that come along with that.

      > I think the reason many game engines are moving away from inheritance is that they're moving away from OO in general

      Games are moving away from OO because C++ style OO is a fundamentally bad way to structure software. Even if it wasn't, struct-of-arrays usually performs better than arrays-of-structs because of how caching works. And modern ECS (entity component systems) can take good advantage of SoA style memory layouts.

      The performance gap between CPU cache and memory speed has been steadily growing over the last few decades. This means, relatively speaking, pointers are getting slower and big arrays are getting faster on modern computers.

      • kragen 2 days ago

        I agree with what you say about ECS and the memory hierarchy. But not much else.

        > inheritance almost always means virtual functions

        Inheritance and "virtual functions" (dynamic method dispatch) are almost, but not completely, unrelated. You can easily have either one without the other. Golang and Lua have dynamic method dispatch without inheritance; C++ bends over backwards so that you can use all the inheritance you want without incurring any of the costs of dynamic method dispatch, as long as you don't declare anything virtual. This is actually a practical thing to do with modern C++ with templates and type inference.

        > No - if you put A and B in separate allocations, you get worse performance

        Yes, that's what I was saying.

        > you're putting more pressure on the allocator / garbage collector

        Yes, I explained how that happens in greater detail in the comment you were replying to.

        With your struct C, it's somewhat difficult to solve the problem catern was saying Simula invented inheritance to solve; if A is "list node" and B is "truck", when you navigate to a list node p of type A*, to get the truck, you have to do something like &((struct C *)p)->b, relying on the fact that the struct's first field address is the same as the struct's address and on the fact that the A is the first field. While this is certainly a workable thing to do, I don't think we can recommend it without reservation on the basis that "you don't get any class entanglement, or any of the bugs"! It's very error-prone.

        > Games are moving away from OO because C++ style OO

        There are a lot of things to criticize about C++, but I think one of its worst effects is that it has tricked people into thinking that C++ is OO. "C++ style OO" is a contradiction in terms. I mean, it's possible to do OO in C++, but the language fights you viciously every step of the way; the moment you make a concession to C++ style, OO collapses.

  • bitwize 3 days ago

    Simple inheritance makes the class hierarchy complicated through issues like the diamond inheritance problem, which C++ resolves in typical C++ fashion: attempt to satisfy everybody, actually satisfy nobody.

    The designers of StarCraft ran into the pitfalls of designing a sensible inheritance hierarchy, as described here (C-f "Game engine architecture"): https://www.codeofhonor.com/blog/tough-times-on-the-road-to-...

    • kragen 3 days ago

      Simple inheritance doesn't have the diamond problem, because that requires multiple inheritance, which isn't simple. Smalltalk doesn't have multiple inheritance; I don't think SIMULA did either.

      • int_19h 3 days ago

        Simula is strictly single inheritance (and no interfaces).

    • nine_k 3 days ago

      The best implementation inheritance hierarchy is none :)

      If you must, you can use the implementation inheritance for mix-ins / cross-cutting concerns that are the same for all parties involved, e.g. access control. But even that may be better done with composition, especially when you have an injection framework that wires up certain constructor parameters for you.

      Where inheritance (extension) properly belongs is the definition of interfaces.

    • virtue3 3 days ago

      really amazing read thank you.

  • wbl 3 days ago

    Dynamic invocation, not strict inheritance is the issue here. Simply getting functions and fields from a superclass costs nothing if at each callsite the compiler knows enough to say where it is from.

    • nine_k 3 days ago

      But this may only happen when no virtual / overridden methods are involved, no VMT to look up in, no polymorphism at play. This is tanamount to composition, which should be preferred over inheritance anyway.

      In this regard, Go and Rust do classes / objects right, Java provides the classical pitfalls, and C++ is the territory where unspeakable horrors can be freely implemented, as usual.

      • wbl 2 days ago

        Overriding is fine. The issue comes with polymorphism and would even without inheritance per se, as can be seen in Go where interfaces provide polymorphism without inheritance.

        • nine_k 2 days ago

          You're right, the issue comes from polymorphism, and from use of polymorphic values when the specific class cannot be determined statically. But this is sort of the point of polymorphism, can't do much about it, except maybe caching the looked-up value(s) when looping over a polymorphic collection.

          Overriding may lead to other troubles though [1].

          [1]: https://www.snopes.com/fact-check/shoot-me-kangaroo-down-spo...

      • vlovich123 3 days ago

        Parent is correct - if the compiler has the information to devirtualize it becomes direct dispatch regardless of the mechanisms involved at the source level. This is also typically true for JITs.

        • nine_k 3 days ago

          This is true as long as the compiler actually can de-virtualize the method, that is, that it can prove that the objects it's handling at a particular call site do not override it. Quite often this is not the case, because of the pattern when the programmer is expected to override / implement an abstract method to plug in the desired functionality. The lookup can be fast if there are few classes involved, and their vtables get cached.

          JITs can do many fascinating optimizations based on profiling the actual code. They must always be on guard though for a case when their profiling-based conclusions fail to hold with some new data, and they have to de-optimize. This being on guard also has its cost.

          • vlovich123 2 days ago

            Even if you call a pure virtual function of an abstract base class, the compiler can devirtualize if it can determine the type anyway due to data flow analysis. There are limitations but with LTO you’d be surprised at how effective the compiler is here. Also in Rust you’re never going through the vtable unless it’s a &dyn Trait and the compiler fails to devirtualize.

  • Reason077 3 days ago

    Nah. Classic C++/Java style inheritance with vtable dispatch is very fast. Generally no slower than a C-style function call, and actually sometimes faster depending on how the C code is linked, characteristics of the CPU, etc.

    • nine_k 3 days ago

      This assumes that the vtables stay in at least L2 cache, which may be a correct assumption for the few hot-path classes. In this regard, I remember how Facebook's android app once failed to build when the codebase exceeded the limit of 64k classes.

      • xxs 3 days ago

        No, Java does Class hierarchy analysis and has multiple way not to use v-table calls.

        Single site (no class found overriding a method) are static and can be inlined directly. Dual call sites use a class check (which is a simple equality), can be inlined, no v-table. 3-5 call sites use inline caches (e.g. the compiler records what class have been used) that are similar and some can be inlined, usually plus a guard check.

        Only high polymorphic calls use v-table and in practice is a very rare occasion, even with Java totally embracing inheritance (or polymorphic interfaces)

        Note: CHA is dynamic and happens at runtime, depending which classes have been loaded. Loading new classes causes CHA to be performed again and if there are affected sites, the latter are to be deoptimized (and re-JIT again)

        • josephg 3 days ago

          Thats very cool.

          Does C++ have any of these optimisations?

          • gpderetta 2 days ago

            Some compilers do some of these optimizations, at least those that not require a JIT and code patching at runtime.

            But these optimizations are vastly less critical in C++ where only are minority of functions are virtual.

        • nine_k 3 days ago

          Thanks, TIL!

    • xxs 3 days ago

      if the dispatches do use vtable they won't be inline and won't be faster. The real deal is inlining when necessary, which inheritance doesn't really prevent.

  • jayd16 2 days ago

    What game engines are moving away from inheritance? Composition is preferred, tho. It's just easier to refactor things that way.

shadowgovt 2 days ago

Fascinating.

Interestingly enough, my first non-class-related experience with "intrusive lists" was in C, and we implemented it via macros; you'd add a LINKED_LIST macro in the body of a struct definition, and it would unspool into the pointer declarations. Then the list-manipulation functions were also macros so they would unspool at compile time into C code that was type-aware enough to know where the pointers lived in that individual struct.

Of course, this meant incurring the cost of a new definition of function families for each intrusive-list structure, but this was in the context of bashing together a demo kernel for a class, so we assumed modern PCs that have more memory than sense. The bigger problem was that C macros are little bastards to debug and maintain (especially a macro'd function... so much escaping).

C++, of course, ameliorates almost all those problems. And replaces them with other problems. ;)

wonderwonder 3 days ago

Favorite part of this is that I had no idea if this article was going to be about biology, code or money. I love a good surprise

  • airstrike 3 days ago

    I literally read it as money first, then code, but clicked thinking it may be biology too

  • hypercube33 3 days ago

    My first assumption was something like ACL lists (though this could be for something like NTFS, or directory permissions) or even Firewall rules but I guess we all bring our background to assumptions

dboreham 2 days ago

Before my time but I reject the idea that inheritance was "invented" in the context of a particular high level programming language. It was used widely in assembly/machine code programming. It's essentially a manifestation of two things: categorization of things (at least thousands of years old); and pointers to data structures (at least as old as the Manchester Mk1).

juped 3 days ago

All of what we take for granted in modern computing architecture was invented as a performance hack by von Neumann in 1945 to take advantage of then-novel vacuum tube tech.

ogogmad 2 days ago

What do folks think of the OCaml/SML style approach with its signatures+modules+functors? It's a bit obscure, and some people find it inconvenient. Inheritance in their approach can be approximated using functors.

austin-cheney 2 days ago

I always thought this was common knowledge. I guess it isn’t.

The only reason inheritance continues to be around is social convention. It’s how programmers are taught to program in school and there is an entire generation of people who cannot imagine programming without it.

Aside from common social practice inheritance is now largely a net negative that has long outlived its usefulness. Yes, I understand people will always argue that without their favorite abstraction everything will be a mess, but we shouldn’t let the most ignorant among us baselessly dictate our success criteria only to satisfy their own inability to exercise a tiny level of organizational capacity.

  • dgb23 2 days ago

    It's not just school. There are a lot pieces of literature, tutorials/guides, discussions, papers I came across over the years that tell you something very useful _plus_ wrap everything either into OO (or sometimes FP) noise and treat this part as just as important. Often there are vague rationales sprinkled in without much backing.

    So you get interfaces that are much bigger than they need to be, visitor pattern this, manager that. As someone who isn't used to OO it is sometimes difficult or cumbersome to compile these kinds of examples and explanations into its essence.

    I also noticed that AI assistants often want to blow up every interface with a whole bunch of useless stuff like getter/setter style functions and the like. That's obviously not the fault of these assistants, but I think it's something to consider.

  • goatlover 2 days ago

    Or because there are some situations where inheritance is useful. There was a reason Simula, Smalltalk, C++, Common Lisp (CLOS), Java, OCaml, Ruby, etc. implemented OOP. That's a lot of different languages. The program designers found it to be a useful abstraction and so did the language users.

    There's no reason to be dogmatic about programming abstractions. Just because OOP became dogma for a while and got abused doesn't mean we have to be dogmatic entirely in the opposite direction. Abstractions have their use for those programming languages that choose to implement them.

    • austin-cheney 2 days ago

      > There's no reason to be dogmatic

      I absolutely disagree. Some things in programming exist to bring products to market, but many things in programming only exist to bring programmers to market. That is a terrible and striking difference that results ultimately from an absence of ethics. Actions/decisions that exist only to discard ethical considerations serve only two objectives: 1) normalization of lower competence, 2) narcissism. It does not matter which of those two objectives are served, because the conclusions are the same either way.

simonebrunozzi 2 days ago

I know we're on Hacker News, but I would have preferred a more explicit title (programming language, not wealth across generations).

  • eterm 2 days ago

    The first six words into the actual article makes it clear what it's about:

    > Inheritance was invented by the Simula language

    • simonebrunozzi a day ago

      Title of the Hacker News submission, not title of the article itself.

adornKey 2 days ago

Performance is not a hack. Title is wrong... ;-)

Interfaces are indeed much nicer, but you have to make sure that your program language doesn't introduce additional overhead.

Don't be the guy that makes Abstract Factory Factories the default way to call methods. Be aware that there are a lot of people out there that would love to ask a web-server for instructions each time they want to call a method. Always remember that the IT-Crowd isn't sane.

steviee 3 days ago

This title is so wild when you read it without the context of software development...

  • edu 2 days ago

    True, before opening it I thought it was about actual transfer of wealth from parents to children. Which also seems likt a big performance hack.

    The "invented" part was suspicious though.

  • reustle 2 days ago

    I also read it in a different context, an interesting thought

masfoobar 3 days ago

I think many developers, especially in the range on 1999-2020, has gone through many pitfalls in programming. More specifically.. OOP.

As someone who was blessed/lucky to learn C and Pascal.. with some VB6.. I understood how to write clean code with simple structs and functions. By the time I was old enough to get a job, I realised most (if not all) job adverts required OOP, Design Patterns, etc. I remember getting my first Java book. About 1,000 pages, half of which was about OOP (not Java directly)

I remember my first job. Keeping my mouth shut and respecting the older, more experienced developers. I would write code the way I believed was correct -- proper OOP. Doing what the books tell me. Doing what is "cool" and "popular" is modern programming. Hiding the data you should not see, and wrapping what you should in Methods... all that.

Nobody came to me and offered guidance but I learned that some of my older codebase with Inheritence, Overrides.. while it was "proper" code, would end up a jumbled mess when it required new features. One class that was correctly setup one day needed to be moved about, affecting the class hierarchy of others. It brings me back to thinking of my earlier programming days with C -- and to have things in simples structs and functions is better.

I do not hate on OOP. Afterall, in my workplace, am using C# or Python - and make use of classes and, at times, some inheritence here and there. The difference is not to go all religious in OOP land. I use things sparingly.

At work, I use what the Companies has already laid out. Typically languages that are OOP, with a GC, etc. I have no problem with that. At home or personal projects, I lead more towards C or Odin these days. I use Scheme from time-to-time. I would jump at the opportunity to using Odin in the workplace but I am surrounded by developers who dont share my mindset, and stick to what they are familiar with.

Overall, his Conclusion matches my own. "Personally, for code reuse and extensibility, I prefer composition and modules."

  • 62951413 2 days ago

    I learned about OOP from a Turbo Pascal v5.5 book circa 1993. Drawing triangles, squares, circles, all the good stuff. Turbo Vision library was a powerful demonstration of the power of OOP which made MSFT MFC look like a mess in comparison.