Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I think that composition is absolutely better than inheritance except for one thing: boilerplate. The issue is that boilerplate is kind of important.

You don't want to litter your code with "f150.ford.car.vehicle.object.move(50, 50)". You can and should re-implement "move" so that you only have to call "f150.move(50, 50)", but that still requires boilerplate, just in the "F150" class.

Often you have class containing all of the functionality of another class, except a bit more functionality. You can always use composition but this happens so often you're creating a lot of boilerplate.

You could develop some other "syntax sugar" to replace inheritance. Maybe Haskell's type-classes are better (although they also kind of use inheritance, since there are subclasses). But chances are you'll go back to something like inheritance, because it's very useful very often.



COM solves this with delegation, where objects can only implement the methods that they care about and delegate everything else to the aggregated type, which provided the full interface.

However, depending on which stack one is using (VB 6, .NET, MFC, ATL, WRL, WinRT), the amount of boilerplate to deal with the runtime differs.


I wish more languages (in fact, any popular languages!) had convenient syntax for this.


Dynamic languages have it, via "doesNotUnderstand" and similar.

Kotlin also provides a way similar to those COM variants,

https://kotlinlang.org/docs/delegation.html#overriding-a-mem...

Best support is probably MOP in Common Lisp, I guess.


The Kotlin version is pretty much what I had in mind, that's pretty nice.


Any time one dares to defend Pascal's "with" statement, a shitstorm starts...


This doesn't inhherently have anything to do with inheritance. Delegation is the compositional solution to this problem and some languages do have built in sugar for that. It usually looks something like:

    class F150(@delegate private val underlying: Car) { ... }
    class F150(private val underlying: Car) : Car by Underlying { ... }
    // etc


In kotlin you can delegate the implementation of an interface to another object. Basically syntatic sugar for the delegation pattern.

https://kotlinlang.org/docs/delegation.html

With it, you F150 can say it implements the "movable" interface, just buy stating which field it contains that implements it, and the you can run "f150.move"


I'd like languages to have some kind of "delegate" functionality, where you can just delegate names to point to nested names without screwing around with ownership - it would just act like a symlink. The scope of that action is limited and clear (and easy for your IDE to understand), and it's explicit that the subclass is still the "owner" of that property, which makes the whole thing a lot easier to navigate.

E.g. something like:

    class MyClass:
        def __init__(self, member_class):
            self.member_class = member_class

        # Delegate one member
        delegate move member_class.position.move

        # Delegate all members
        delegate * subclass.position.*             

Then:

    a.move == a.member_class.position.move
etc.


C++ can do something something like this (at compile time) in its -> operator (ancient feature, long before C++98 was standardized).

   obj->foo()
will expand into enough -> dereferences until a foo is found. For instance suppose the object returned by obj's operator ->() function doesn't have a foo member, but itself overloads ->. Then that overload will be used, and so on.


In Python you could do something like:

  class Base:
     def func(self):
         print("In Base.func:", self.name)
  
  class Child:
     def __init__(self, name):
         self.name = name
     func = Base.func
  
  c = Child("Foo")
  c.func() #=> In Base.func: Foo


The reason I'd like the construct is because it's explicit - intent (and the scope/limit of your intent) is encoded in what you create. It's clear you intend to do nothing with that name except symlink to the nested member, so the reader doesn't have to anticipate other behaviour (and can't accidentally do something else with it). Generic assignment doesn't convey the same restricted intent, and it doesn't carry those guard rails.

Really though it's a structure that only makes sense in strongly typed languages, so I probably shouldn't have used Python to illustrate the idea.


I'd like to point out that the article isn't disagreeing with you. It's saying inheritance is a dangerous interface for other users of your code (across packages is their terminology). So, if you write a library, maybe don't design it around extending classes. This is a much milder stance than the title implies, and seems pretty reasonable to me.

Edit: Totally with you on boilerplate though. +1.


That's not general [implementation] inheritance, it's just delegation. The problematic, non-compositional feature of implementation inheritance is something known as open recursion, viz. the fact that every call to an overridden method like .move(...) - importantly, even a call that's merely a private implementation detail of some code in the base class - goes through an implied dispatch step that introduces a dependency on the actual derived class instance that the base-class method is being called on. This creates the well-known "fragile base class" problem since method calls to these possibly-overridden methods are relying on fragile, unstated invariants that might be broken in the derived classes, or altered in future versions of the base class.


Boilerplate is a solved[0] problem since a _very_ long time and is orthogonal to the inheritance vs composition problem.

[0] https://en.wikipedia.org/wiki/Macro_(computer_science)


For the same reason, I'm not so absolutist about DRY. Having the most elegant codebase also often means the codebase that's hardest to work on, and it's often better to clean things up afterwards once you know how things will be structured.


This question determines if you need to be DRY or not:

"If [some fact] in the code base needs to change, how many places would we have to change it in?"

If the answer is > 1, you have a very good DRY case. Otherwise, when [some fact] changes, it will probably not be changed in one of the places, and the system will be broken.

This often coincides with having an "elegant codebase", but that's not the most important part.


A younger self used to be very strict about DRY.

For a living codebase, nowadays my general rule of thumb is to consciusly duplicate code until it covers 3 different cases, and only then refactor (unless the DRY way is as fast and obvious).

It takes more than that to yield spaghetti and a lot of time is saved on premature generalization. Plus the generalization is often way more straightforward once the explicit cases are already implemented.


https://en.wikipedia.org/wiki/Rule_of_three_(computer_progra...

I follow this too for "style" refactorings.


The original formulation of DRY was "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system," which in import winds up pretty close to what you have here.


Recently (last month basically), i rewrote my code (i'm leaving soon and i want my coworkers/successors to have success improving on what i've done).

I followed every principle of good code, except one, DRY. I tried to make generic parts to connectors, because they do have similarities. But this is a work of at least a year, and the price to make it generic was increasingly more complex configuration files (Just the pagination alone added 3 variables for two different APIs, and the number of app i am supposed to interact with should grow to ~40). I decided after a few days of reflexion that the idea was not that dumb in principle, but unworkable in my case, and decided that one connector for one API, even with a lot of repetition.


Yeah, I had a few connectors (API clients to different APIs with some business logic wrappers to handle low level stuff) and decided that they should share code with a generic connector interface, and then when one of them changed it was pretty painful untangling it. There is a tradeoff between getting stuff done and abstraction engineering, and there are costs to premature abstraction engineering.


See also: the wrong abstraction is worse than no abstraction at all.

https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction



Go solves this with problem with embedding. If a type is imbedded inside of a struct and has its own methods, those methods are implicitly available on the new struct.




Consider applying for YC's Winter 2026 batch! Applications are open till Nov 10

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: