Comment by lukan

4 days ago

Out of curiosity, when you say you don't like inheritance, does that mean you never use it at all, or you only use it rarely?

Because even though inheritance often is used in a wrong way, there are definitely cases, where it is the clearest pattern in my opinion.

Like graphic libary things. E.g. everything on the screen is a DisplayObject. Simple Textfields and Images inherit directly from DisplayObject. Layoutcontainers inherit from DisplayObjectContainer which inherits from DisplayObject.

Inheritance here makes a lot of sense to me and I don't see how it could be expressed in a different way without loosing that clarity.

> I don't see how it could be expressed in a different way without loosing that clarity.

What value does the inheritance provide here?

Can't you just use a flat interface per usecase without inheritance and it will work simpler with less mental overhead keeping the hierarchy in mind?

Explicitly your graphic library sounds should be fine to have the interface DisplayObject which you can then add default implementations on. (That's a form of composition)

  • That would be way, way more verbose for everything.

    Every display object has a x y width and height for example. And there is basic validating for every object. Now a validate method can be conposited. But variables? Also the validating, there is some base validating every object share (called wih super) and the specific validating (or rendering) is done down in the subclasses.

    And even for simple things, you can composite a extra object, but then you cannot do a.x = b.x * 2 anymore, but would have to do a.po.x = b.po.x * 2 etc

    • > Every display object has a x y width and height for example.

      Intuitively¹, I feel like this is something that should be separated out into a BoundingBox object. Every component that needs a bounding box satisfies a small `HasBoundingBox { getBoundingBox(self) -> BoundingBox }` interface. Maybe there's a larger `Resizeable` interface which (given a type that satisfies `HasBoundingBox`) specifies an additional `setBoundingBox(self, bb)` method.

      You don't end up with a tidy hierarchy this way, but I'm not sure you'd end up with a tidy hierarchy using inheritance, either. I feel like this sort of UI work leads toward diamond inheritance, mixins, or decorators, all of which complicate inheritance hierarchy. Flat, compositional design pushes you toward smaller interfaces and more explicit implementations, and I like that. The verbosity can be kept in check with good design, and with bad design, the failure mode leans towards more verbosity instead of more complexity.

      For more complicated features, composition & interfaces can make things more verbose, but honestly I like that. Inheritance's most powerful feature is open recursion (defined in the linked article), and I find open recursion to be implicit and thorny. If you need that level of power, I'd rather the corresponding structure be explicit, with builders and closures and such.

      [1]: Not saying this is correct, but as someone who prefers composition to inheritance, this is what feels natural to me.

      3 replies →

    • To avoid splitting the discussion by responding directly to your comment above, since I have thoughts about this one as well: I've written Rust professionally since 2019, which doesn't have inheritance, so I don't use it at all. I guess my point is that I don't miss having inheritance as a tool in my everyday coding, and I actively prefer not having it available in Rust.

      In terms of what you're saying here, the extra verbosity is not really something that either bothers me or is impossible to work around in the context of Rust at least. The standard library in Rust has a trait called `Deref` that lets you automatically delegate method calls without needing to specify the target (which is more than sufficient unless you're trying to emulate multiple inheritance, and I consider not providing support for anything like that a feature rather than a shortcoming).

      If I were extremely bothered by the need do do `a.po.x` in the example you give, I'd be able to write code like this:

          struct Point {
              x: i32,
              y: i32,
          }
      
          struct ThingWithPoint {
              po: Point,
          }
      
          impl Deref for ThingWithPoint {
              type Target = Point;
      
              fn deref(&self) -> &Self::Target {
                  &self.po
              }
          }
      
          fn something(a: &mut ThingWithPoint, b: ThingWithPoint) {
              a.x = b.x * 2;
          }
      

      Does implementing `Deref` require a bit more code than saying something like `ThingWithPoint: Point` as part of the type definition? Yes (although arguably that has as much to do with how Rust defines methods as part of `impl` blocks outside of the type definition, so defining a method that isn't part of a trait would still be a slightly more verbose, and it's not really something that I particularly have an issue with). Do I find that I'm unhappy with needing to be explicit about this sort of thing rather than having the language provide an extremely terse syntax for inheritance? Absolutely not; the extra syntax convenience is just that, a convenience, and in practice I find it's just as likely to make things more confusing if used too often than it is to make things easier to understand. More to the point, there's absolutely no reason that makes sense to me why the convenience of syntax needs to be coupled with a feature that actually changes the semantics of the type where I want that convenience; as comment I originally replied to stated, inheritance tries to address two very different concerns, and I feel pretty strongly that ends up being more trouble than it's worth compared to just having language features that address them separately.