6 ms·
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
by armchairhacker 5y ago
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.
- MillenialMan 5y agoI'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.
- nitroll 5y agoIn 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
- MillenialMan 5y agoThe 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.
- kazinator 5y agoC++ 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.
- deleted 5y ago[deleted]
- esarbe 5y agoCheck out Scala 3's 'export' feature. https://docs.scala-lang.org/scala3/reference/other-new-features/export.html https://docs.scala-lang.org/scala3/reference/other-new-featu...
- w-j-w 5y agoGo 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.
- rsj_hn 5y agoFor 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.
- wk_end 5y agoSee also: the wrong abstraction is worse than no abstraction at all. https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction
- BurningFrog 5y agoThis 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.
- dllthomas 5y agoThe 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.
- 3np 5y agoA 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.
- BurningFrog 5y ago
- PhineasRex 5y agoThis 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
- caseymarquis 5y agoI'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.
- zozbot234 5y agoThat'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.
- pjmlp 5y agoCOM 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.
- iainmerrick 5y agoI wish more languages (in fact, any popular languages!) had convenient syntax for this.
- pjmlp 5y agoDynamic 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-member-of-an-interface-implemented-by-delegation https://kotlinlang.org/docs/delegation.html#overriding-a-mem... Best support is probably MOP in Common Lisp, I guess.
- iainmerrick 5y agoThe Kotlin version is pretty much what I had in mind, that's pretty nice.
- CRConrad 5y agoAny time one dares to defend Pascal's "with" statement, a shitstorm starts...
- dgb23 5y agoBoilerplate 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) https://en.wikipedia.org/wiki/Macro_(computer_science)
- tm-guimaraes 5y agoIn 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 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"