Live data from Hacker News

Why general inheritance is flawed and how to finally fix it

minborgsjavapot.blogspot.com

91–100 of 104 posts

Re: Why general inheritance is flawed and how to finally fix it

#91

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 which is used as such: subset = Subset::Whole…

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…

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

Re: Why general inheritance is flawed and how to finally fix it

#92

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 which is used as such: subset = Subset::Whole…

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.

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.

Re: Why general inheritance is flawed and how to finally fix it

#93
post #62

Earlier quoted context omitted.

> Spring does (and perhaps even prefers where possible) constructor-based inheritance though OFC Spring does lots of things (too many things), but that varies project to project based on what series of Spring-related libraries are being used. More likely I'll see non Spring-core annotations like @RestController + @RequestMapping-attributes and have to figure a standard way to mock up some of what Spring does just to…

Testing REST-endpoints should be the job of integration tests, which are by definition more involved. Also, spring has really great test suits for these use cases. Other classes/components should in the general case be written as POJOs. The dependent components can be mocked/injected simply by using the constructor.

> Testing REST-endpoints should be the job of integration tests, which are by definition more involved.

I would confidently say, this philosophy is dead wrong. Integration test are useful, but you still want the unit tests to ensure that the code paths and side effects are maintained. With runtime composition, this is much harder. Java sacrificed what we do know for a grand experiment of doing as much as possible in pre-processing, breaking (sacrificing) the known concept of code reliability in the hopes that someone else would figure out a way to handle the testing implications down the line. Java Testing went from a gold standard to an afterthought with annotations. This is how important getting additional composition turned out to be, but at what cost?

> spring has really great test suits for these use cases.

@SpringBootTest requires booting up Spring just to add in the runtime composition. It's both unnecessarily time consuming and problematic to have to predict composition rather than observing it directly. Now you have to memorize what Spring might do, given annotations that can be anywhere AND the code you are trying to test. Nightmare stuff.

Re: Why general inheritance is flawed and how to finally fix it

#94
post #72

Earlier quoted context omitted.

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

But this is an issue with tooling. IntelliJ with Java/Kotlin does a great job here.

Unsolved AFAIK, Reducing the ergonomics of the language which is an important point

Re: Why general inheritance is flawed and how to finally fix it

#95
post #62

Earlier quoted context omitted.

Testing REST-endpoints should be the job of integration tests, which are by definition more involved. Also, spring has really great test suits for these use cases. Other classes/components should in the general case be written as POJOs. The dependent components can be mocked/injected simply by using the constructor.

> Testing REST-endpoints should be the job of integration tests, which are by definition more involved. I would confidently say, this philosophy is dead wrong. Integration test are useful, but you still want the unit tests to ensure that the code paths and side effects are maintained. With runtime composition, this is much harder. Java sacrificed what we do know for a grand experiment of doing as much as possible in…

How would you test a REST endpoint, if I may ask? Because in the end it will somehow reply to a request. But that response has quite a few things going on — if you give back the url of a templates string as a constant, is that meaningful to unit test that? For anything more complex you should be writing a service which can and should be unit tested. But I believe that the set of headers, security! and the like is not in the realm of the quite complex job of endpoints. By that you would be testing the Spring library, which presumably happens on spring’s side.

Re: Why general inheritance is flawed and how to finally fix it

#96

Earlier quoted context omitted.

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

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.

[deleted]

Re: Why general inheritance is flawed and how to finally fix it

#97
post #85

Earlier quoted context omitted.

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

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.

Re: Why general inheritance is flawed and how to finally fix it

#98
post #34

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 de…

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.

Is Fortress the same language that required exponential time constraint solving for its type system?

Re: Why general inheritance is flawed and how to finally fix it

#99

Earlier quoted context omitted.

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.

Is Fortress the same language that required exponential time constraint solving for its type system?

Type checking is not exponential last I checked. However, languages with global type inference have exponential behaviour when inferring types for some pathological programs.

Re: Why general inheritance is flawed and how to finally fix it

#100
post #95

Earlier quoted context omitted.

> Testing REST-endpoints should be the job of integration tests, which are by definition more involved. I would confidently say, this philosophy is dead wrong. Integration test are useful, but you still want the unit tests to ensure that the code paths and side effects are maintained. With runtime composition, this is much harder. Java sacrificed what we do know for a grand experiment of doing as much as possible in…

How would you test a REST endpoint, if I may ask? Because in the end it will somehow reply to a request. But that response has quite a few things going on — if you give back the url of a templates string as a constant, is that meaningful to unit test that? For anything more complex you should be writing a service which can and should be unit tested. But I believe that the set of headers, security! and the like is not…

> How would you test a REST endpoint, if I may ask?

Functional test (or integration test if you like). That wasn't the point.

> For anything more complex you should be writing a service which can and should be unit tested.

You're special casing a function based on how it's placed in the flow of the project. Unit (fn being a unit) tests are necessarily agnostic as to the overall functionality. If you want to special case how you are handling functions in a project, good luck. You'll continue to have failures that you'll handwave away as "not following the patterns" or "simple mistakes" rather than recognizing that you should have had useful unit tests to prevent it.

People often conflate the reasons that unit testing doesn't prevent bugs. The primary weakness of unit testing is that you cannot assert "no additional functionality" in the function implementation (code). When languages adopt testing as a first class concern and provide function hashes as a validation, we will see it adopted en-mass and these runtime compositions strategies will be left out, and rightly so.

Post reply on HN