Readit News logoReadit News
armchairhacker · 4 years 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.

pjmlp · 4 years ago
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.

iainmerrick · 4 years ago
I wish more languages (in fact, any popular languages!) had convenient syntax for this.
PhineasRex · 4 years ago
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

tm-guimaraes · 4 years ago
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"

MillenialMan · 4 years ago
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.

kazinator · 4 years ago
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.

nitroll · 4 years ago
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

caseymarquis · 4 years ago
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.

zozbot234 · 4 years ago
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.
dgb23 · 4 years ago
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)

rsj_hn · 4 years ago
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.
BurningFrog · 4 years ago
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.

orwin · 4 years ago
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.

wk_end · 4 years ago
See also: the wrong abstraction is worse than no abstraction at all.

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

esarbe · 4 years ago
w-j-w · 4 years ago
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.
nickm12 · 4 years ago
https://lwn.net/Articles/548560/

I really enjoyed the article above, which I read many years ago (before Rust 1.0!) which discusses how Golang and Rust handle polymorphism and code-reuse without classic object inheritance. My current thinking is that software objects are a general-purpose tool, but classic object inheritance should rarely be used as it is a solution to a narrow problem—classes should be "final" by default, and if not the inheritance pattern should be completely designed up front.

Java had the misfortune to be designed at a time when OOP was the new craze and the design decision to force all code into an object hierarchy has not held up well. I'd rather use languages designed either before or after Java, where you can use objects when they are appropriate and ignore them when they aren't.

baq · 4 years ago
I mostly agree, but there's one place where it does make a lot of sense to keep the hierarchy open: exceptions. Ability raise a specific error and catch it in a generic handler is very useful.
snovv_crash · 4 years ago
Interfaces would work fine here as well.
nickm12 · 4 years ago
Agreed. Exceptions tend to trivially respect the Liskov substitution principle and generally hold little state besides a debugging string. It's when you subclass an object as a method of code reuse that you start running into problems.
naasking · 4 years ago
Arguably, subtyping in am OO language should either be signatures/interfaces only, or you should go full blown multiple inheritance for everything, as with the Fortress language.
zaphirplane · 4 years ago
Golang ide struggle with answering what implements this interface. The compiler obviously handles that fine

It makes it difficult to jump into an unfamiliar project

Assuming that’s what you mean by signature/ interfaces

klibertp · 4 years ago
Is Fortress the same language that required exponential time constraint solving for its type system?
nauticacom · 4 years ago
I (happily) write a lot of OOP code, "inheritance is bad, use composition" is such a trite and unhelpful dogma that gets in the way of any actual discussion about where inheritance is useful.

IMO, the case where inheritance makes the most sense is when you have a set of objects polymorphically answering some question, usually with a simple answer.

    class Subset
        class Whole < Subset
            def of(items)
                items
            end
        end

        class Range < Subset
            def initialize(from:, to:)
                @from = from
                @to   = to
            end

            def of(items)
                items[@from:@to]
            end
        end
    end
which is used as such:

    subset = Subset::Whole.new
    puts subset.of(["a", "b", "c"]) # => ["a", "b", "c"]

    subset = Subset::Range.new(from: 0, to: 1)
    puts subset.of(["a", "b", "c"]) # => ["a"]
You can then pass around a Subset object anywhere (aka dependency injection) and push conditionals up the stack as far as possible.

Simply saying "inheritance is bad" gets nobody anywhere.

BorisTheBrave · 4 years ago
In most languages Subset would be called a trait or interface, rather than general inheritance. You've picked an example with no fields or overriden methods, so it's impossible for it to demonstrate the shortcomings of inheritance.
ridiculous_fish · 4 years ago
What is the practical difference between inheritance, and a trait or interface with a default implementation? It seems like both risk the addAll() bug.
orwin · 4 years ago
OP title said "general inheritance is bad", not " Generally, inheritance is bad".

And the text support that. The "general inheritance" the author describe is not the one you've just used.

And i'm hijacking your post, sorry, but i really agree with the author with the "incidental inheritance" point. This is the worst. I lost a month to a bug caused by this kind of inheritance (Jenkins package that tried to be cute and interfered with a cloudbees class). I won't take a java gig ever again. Not worth the brain damage.

devoutsalsa · 4 years ago
The thing I don’t like about passing objects around is that the state inside the object is opaque, and debugging it can be extremely frustrating, especially in something like Ruby some people are way too liberal with magic for my taste. My personal preference is to see immutable data structures being passed around through reasonably named functions, and that the is usually good enough for me.
nauticacom · 4 years ago
The thing I like about passing objects around is that the state inside the object is opaque :). Thus when changes to the internal details of Person happen, the behavior of which is depended on by Inbox and Message, as long as I have properly depended on its public behavior, I don't need to change anywhere else. If I was just using plain data values as is common in e.g. Clojure, every change to something's internal representation would require changes to places which depend on it.
cies · 4 years ago
In a language where most errors will be runtime errors (Ruby), the inheritace problem described in the article is much less of a problem.

In such a language (e.g. Ruby), you will need test suites where languages with (strong) types use the type system to prove some level of correctness.

I used to be a fan of dyn typed langs (Ruby), but I've changed, I prefer strongly typed langs now for anything more than quick throw away scripts.

bruce343434 · 4 years ago
that approach gives me headaches to think about. Why not just have polymorphic functions?

    fn subset(superset, start, end){
        // superset is type inferred as long as it supports the [] operator
        // logic to collect superset[start] to superset[end] into an array and return it
    }
with uniform function call syntax:

    [1,2,3,4,5,6].subset(1,4) == [2,3,4,5]
If you really want to reuse a subset range, you can use lambdas/closures, or in this case a simple wrapper

    // in some code
    fn subset1to4(superset){
        return subset(superset,1,4)
    }
    array.subset1to4()
    anotherArray.subset1to4()

nauticacom · 4 years ago
Sure, that works for some specific problems where you're computing a value from a defined set of data types. "Subset of this data" was an example I've encountered in the past and used here because it has clearly distinct cases—give me the whole thing, give me some index-delimited range, possibly others—but there are plenty of other examples that don't fit a polymorphic function model (and let's forget that I've never even used a language with polymorphic functions).

As another example I've encountered in the past, let's say you have some object that can dynamically define fields. Once you define a field, you can retrieve its value or maybe some default value e.g.

    model = Model.new
    model.define("points", default: 1)
    model.store("points", 10)
    points = model.retrieve("points")
    puts points # => 10
Let's say doing anything with an undefined field is invalid. Here's my first pass at an implementation:

    class Model
        def initialize
            @fields = {}
        end

        def define(name, default: nil)
            @fields[name] = Field.new(name, default)
        end

        def retrieve(name)
            @fields[name].value
        end

        def store(name, value)
            @fields[name].value = value
        end
    end

    class Field
        attr_reader :name
        attr_accessor :value

        def initialize(name, value)
            @name  = name
            @value = value
        end
    end
Works great! One day a requirement comes along that default values need to be lambdas, too, which are called every time the value is retrieved. How do we implement that? One way is to add a conditional to the Field class:

    class Field
        attr_reader :name
        attr_writer :value

        def initialize(name, value)
            @name  = name
            @value = value
        end

        def value
            if value.is_a?(Proc)
                @value.call
            else
                @value
            end
        end
    end
But now Field knows that it can be passed a lambda, so testing it needs to account for that case (among many other considerations, probably, in a real-world system). And any time we add more cases for default values, let alone changes to regular values like type casting or something, the Field class becomes more complicated. I'd probably reach for a new object instead:

    class Model
        def initialize
            @fields = {}
        end

        def define(name, default: nil)
            @fields[name] = Field.new(name, nil, Default.for(default))
        end

        def retrieve(name)
            @fields[name].value
        end

        def store(name, value)
            @fields[name].value = value
        end
    end

    class Field
        def initialize(name, value, default)
            @name    = name
            @value   = value
            @default = default
        end

        def value
            if @value.nil?
                @default.value
            else
                @value
            end
        end
    end

    class Default
        def self.for(indicator)
            if indicator.is_a?(Proc)
                Default::Dynamic.new(indicator)
            elsif indicator.nil?
                Default::None.new
            else
                Default::Static.new(indicator)
            end
        end

        class Static < Default
            def initialize(value)
                @value = value
            end

            def value
                @value
            end
        end

        class Dynamic < Default
            def initialize(callable)
                @callable = callable
            end

            def value
                @callable.call
            end
        end

        class None < Default
            def value
                nil
            end
        end
    end
Now we've changed the conditional in the Field class to one that's actually relevant to it (do I have a value yet?) and won't change when the kinds of default values that it can accept change. Because we dependency-injected the Default object into the Field object, testing that conditional becomes a binary of retrieving the default value when no value is set, and retrieving the value once it's set. We can then test each kind of Default on its own, and changes to Default don't impact Field. If we really, really wanted to we could even eliminate the conditional in Field alltogether by unifying the interface for @default and @value such that they're both objects with a #value method (or maybe rename it to something else so we don't write @value.value). In either case we've made each piece simpler to reason about and pushed conditionals up the call stack so the resulting code is more straightforward.

I can probably recall more examples of simplifications like this, but this is where I find inheritance the most useful: a known set of things that each polymorphically conform to some interface. In these examples I don't actually use the superclass for any shared behavior, but you can imagine a case where I might.

One other benefit that I really like from the inheritance-object-modeling-as-pushing-up-conditionals perspective is that it makes you define what the different cases of something are as distinct objects, and give names to them. It's a similar benefit that falls out of using named sum types instead of signal values or tagged unions or something, but has the opposite effect (overall reduction of conditionals rather than proliferation).

Deleted Comment

zinxq · 4 years ago
The term "general inheritance" was not familiar to me as "inheritance across package structures". However, my OOP design intuition feels pretty good about that idea.

This quickly devolves into the inheritance vs. composition argument which isn't where I thought the Author wanted to go (but then sort of ended up going there). I agree with other commenters that it's an overstated idea. Inheritance is ridiculously useful in the right design structure, as is Composition. They both have a place. (Incidentally, bad Inheritance design usually looks very ugly very fast - bad Composition is often less glaring).

I find that years of designing in OOP has led me to build designs that have a goal of preventing me from making future mistakes and correctly consider implications of my code.

I find that my most immediate designs tend me towards Abstract Classes and Interfaces. While I usually get credit for "programming to the Interface" for this, that's not what usually led me there.

I like abstract methods. They (i.e. the compiler will) FORCE me to think about something if I ever decide to create another subclass of the Abstract class. The Author points out the "forget to call super" bug which is particularly nefarious and I avoid it at all costs. I can do that by providing a final concrete method which calls the abstract method. Let the subclasses implement that and never worry about super.

Anyway - governing inheritance across package hierarchies seems like a reasonable guideline. As for Inheritance vs. Composition, I don't favor either. When designing a class structure, I just make my best guess (as we'd all do) and find the structure quickly evolves on it's own. Usually, this ends up in a blend of shallow Inheritance trees with logical composition. There's always multiple Class Structures that will work - my goal is to find a reasonable one of those.

josephg · 4 years ago
> Inheritance is ridiculously useful in the right design structure

I’ve made very little use of inheritance since I turned my back on C++/Java a decade and change ago. Can you give some examples where you feel inheritance wins out over composition?

ncmncm · 4 years ago
Inheritance is flawed, in Java, mainly because it is the only organizing principle offered, so gets shoehorned into all kinds of problems where it is a poor fit.

Inheritance is just the right thing once in a while, but Java coders are obliged to apply it well beyond its useful range.

kaba0 · 4 years ago
Just because it exists doesn’t mean it has to be used beyond its intended domain. One is entirely free to create flat “hierarchies” in Java. But I agree that in hindsight, final classes as a default would be better.

Fortunately nowadays, records and sealed classes remedy this for the most part in java.

ivanche · 4 years ago
What do you mean by "obliged to apply it"? For example, I very very rarely apply it and nobody is forcing me to create inheritance hierarchy.
AmericanBlarney · 4 years ago
The argument is "because it's possible to do misuse with inheritance, you should never use it".

By extension then, because it's possible to misuse Java/any programming language/computers/electricity/etc., you should never use it.

stevenalowe · 4 years ago
Or you could learn to use it properly.

Make no mistake, designing classes to support inheritance is much harder than just declaring everything final, and in many scenarios there is no good reason to do so

alkonaut · 4 years ago
Isn't the "use it properly"-argument pretty much the same arguments as those saying that real C developers don't need the safeties offered by rust, they just need to use C or C++ properly?

The whole idea of language design (in my opinion) is to reduce the opportunities for mistakes, without getting in the way (thus reducing productivity). The biggest problem with Java and C# is that they are deceiptively simple. Anyone can get off the ground and the path of least resistance initially is the path of maximum pain in the end. That's the path of making large classes, lots of mutable state, long inheritance chains and so on. The languages aren't forcing anyone to use these antipatterns, but neither are they guiding the hand of the newcomer not to do that.

p2t2p · 4 years ago
I _hate_ smart asses that do "everything final by default". It all fun and giggles until I can't mock some stupid class in some stupid library that I have no choice but to use just because someone is high on "inheritance is bad" hype. Instead of normal mocking/stubbing I now have to use stuff like PowerMock which does byte code hacking just so I can have a test.

How about you stop making decisions for me and let _me_ decide whether I want to inherit your class or not.

andrekandre · 4 years ago

  > I can't mock some stupid class in some stupid library that I have no choice but to use just because someone is high on "inheritance is bad" hype.
yea, at the very least, classes public members should be more like interfaces, that way mocking can be done easily in test mode, then in prod build lots of optimizations could "dissolve" the interface and be statically dispatched etc... hmmm....

BlackFly · 4 years ago
The final keyword is one of those places where Java shows its age to me. I agree with the overall point that inheritance is flawed, but I cannot bring myself to conclude that the use of final is the answer to the problem.

Simple example, String is final in Java. It is also immutable, and that is (mostly) irrelevant. Lots of string fields on inbound requests have validations, a simple one would be a field that contains a fixed length string. So obviously you validate that at the ingress before passing it down. Now, the question arises, should the core library be defensive and re-validate the string? Why not simply capture the subtype, TenCharacterString and parameterize methods with that?

Modern languages get this right. Subtyping is not inheritance. Inheritance is not subtyping. I should be able to subtype at zero cost, I don't need inheritance to do that, [and encapsulation is definitely not subtyping].

But Java doesn't have that. You mark something as final and you lose the ability to subtype just to eliminate the possibility of inheritance. On the other hand, to be fair to the argument against final, the real answer to my complaint is a proper type aliasing support.

rzzzt · 4 years ago
The "extends" keyword gives it away that inheritance in Java was not positioned to support the restrictive cases you have in mind (Square : Rectangle, NegativeNumber : Number, TenCharacterString : String).

What issues do you see with wrapping? TenCharacterString eg. could use char[] as its backing store and implement CharSequence if you want to get it to speak a common language with String.