Fixing inheritance?

First, there was a long introduction about how I came up with a brilliant idea (joke, these are mixins in TS / JS and Policy in C ++ ), which the article is about. I will not waste your time, here is the hero of today's celebration (carefully, 5 lines in JS):



function Extends(clazz) {
    return class extends clazz {
        // ...
    }
}


Let me explain how it works. Instead of regular inheritance, we use the mechanism above. Then we specify the base class only when creating an object:



const Class = Extends(Base)
const object = new Class(...args)


I will try to convince you that this is the son of my mother's friend for class inheritance and a way to return inheritance to the title of true OOP tool (right after prototypal inheritance, of course).



Almost not offtopic
, , , pet project , pet project'. , .





Let's agree on the names: I will call this technique a mixin, although this still means a little different . Before I was told that these are mixins from TS / JS, I used the name LBC (late-bound classes).







The "problems" of class inheritance



We all know how "everyone" "hates" class inheritance. What are his problems? Let's figure it out and at the same time understand how mixins solve them.



Implementation inheritance breaks encapsulation



The main task of OOP is to bind together data and operations on it (encapsulation). When one class inherits from another, this relationship is broken: data is in one place (parent), operations in another (inheritor). Moreover, the inheritor can overload the public interface of the class, so that neither the code of the base class, nor the code of the inherited class separately can tell what will happen to the state of the object. That is, the classes are coupled.



Mixins, in turn, greatly reduce coupling: on the behavior of which base class should the inheritor depend on, if there is simply no base class at the time of declaring the inheriting class? However, thanks to late-bound this and method overloading, the "Yo-yo problem"remains. If you use inheritance in your design, from it can not escape, but, for example, in Kotlin keywords openand overrideshould greatly ease the situation (I do not know, not too closely acquainted with Kotlin).



Inheriting unnecessary methods



A classic example with a list and a stack: if you inherit the stack from a list, methods from the list interface will get into the stack interface, which can violate the stack invariant. I would not say that this is an inheritance problem, because, for example, in C ++ there is private inheritance for this (and individual methods can be made public using using), so this is rather a problem of individual languages.



Lack of flexibility



  1. , : . , , : , , cohesion . , .
  2. ( ), . , : , .
  3. . - , . , , ? - β€” , .. .
  4. . - , - . , , .




If a class inherits from an implementation of another class, changing that implementation may break the inheriting class. In this article there is a very good illustration of the problems Stackand MonitorableStack.



With mixins, the programmer must take into account that the inheriting class he writes must work not only with some specific base class, but also with other classes that correspond to the interface of the base class.



Banana, gorilla and jungle



OOP promises composability, i.e. the ability to reuse individual classes in different situations and even in different projects. However, if a class inherits from another class, in order to reuse the inheritor, you need to copy all the dependencies, the base class and all its dependencies, and its base class…. Those. wanted a banana, and pulled out a gorilla, and then a jungle. If the object was created with the Dependency Inversion Principle in mind, the dependencies are not so bad - just copy their interfaces. However, this cannot be done with the inheritance chain.



Mixins, in turn, make it possible (and obligatory) to use DIPs in relation to inheritance.



Other amenities of the Mixins



The advantages of mixins do not end there. Let's see what else you can do with them.



Death of the inheritance hierarchy



Classes no longer depend on each other: they only depend on interfaces. Those. the implementation becomes the leaves of the dependency graph. This should make refactoring easier - the domain model is now independent of its implementation.



Death of abstract classes



Abstract classes are no longer needed. Let's look at an example of the Factory Method pattern in Java borrowed from the refactoring guru :



interface Button {
    void render();
    void onClick();
}

abstract class Dialog {
    void renderWindow() {
        Button okButton = createButton();
        okButton.render();
    }

    abstract Button createButton();
}


Yes, of course, Factory Methods evolve into Builder and Strategy patterns. But you can do this with mixins (let's imagine for a second that Java has first-class mixins):



interface Button {
    void render();
    void onClick();
}

interface ButtonFactory {
    Button createButton();
}

class Dialog extends ButtonFactory {
    void renderWindow() {
        Button okButton = createButton();
        okButton.render();
    }
}


You can do this trick with almost any abstract class. An example where it doesn't work:



abstract class Abstract {
    void method() {
        abstractMethod();
    }

    abstract void abstractMethod();
}

class Concrete extends Abstract {
    private encapsulated = new Encapsulated();

    @Override
    void method() {
        encapsulated.method();
        super.method();
    }

    void abstractMethod() {
        encapsulated.otherMethod();
    }
}


Here the field is encapsulatedneeded in both overload methodand implementation abstractMethod. That is, without breaking the encapsulation, the class Concretecannot be divided into a child Abstractand a "superclass" Abstract. But I'm not sure if this is an example of good design.



Flexibility comparable to types



The attentive reader will notice that these are all very similar to Smalltalk / Rust traits. There are two differences:



  1. Mixin instances can contain data that was not in the base class;
  2. Mixins do not modify the class they inherit from: to use the mixin functionality, you need to explicitly create a mixin object, not a base class object.


The second difference leads to the fact that, let's say, mixins act locally, in contrast to traits that act on all instances of the base class. How convenient it is depends on the programmer and on the project, I won't say that my solution is definitely better.



These differences bring mixins closer to normal inheritance, so this thing seems to me to be a funny compromise between inheritance and traits.



Cons of mixins



Oh, if only it were that simple. Mixins definitely have one small problem and one fat minus.



Exploding interfaces



If you can inherit only from the interface, obviously, the project will have more interfaces. Of course, if the DIP is respected in the project, a few more interfaces will not do the weather, but not all follow SOLID. This problem can be solved if, based on each class, an interface containing all public methods is generated, and when mentioning the class name, distinguish whether the class is meant as a factory of objects or as an interface. Something similar is done in TypeScript, but for some reason private fields and methods are mentioned in the generated interface.



Complex constructors



Using mixins, the most difficult task is to create an object. Consider two options depending on whether the constructor is included in the base class interface:



  1. , , . , - . , .
  2. , . :



    interface Base {
        new(values: Array<int>)
    }
    
    class Subclass extends Base {
        // ...
    }
    
    class DoesntFit {
        new(values: Array<int>, mode: Mode) {
            // ...
        }
    }
    


    DoesntFit Subclass, - . Subclass DoesntFit, Base .
  3. In fact, there is another option - to pass to the constructor not a list of arguments, but a dictionary. This solves the problem above, because it { values: Array<int>, mode: Mode }obviously fits the pattern { values: Array<int> }, but it leads to an unpredictable collision of names in such a dictionary: for example, both the superclass Aand the inheritor Buse the same parameters, but this name is not specified in the interface of the base class for B.


Instead of a conclusion



I'm sure I missed some aspects of this idea. Or the fact that this is already a wild button accordion and twenty years ago there was a language using this idea. In any case, I'm waiting for you in the comments!



List of sources



neethack.com/2017/04/Why-inheritance-is-bad

www.infoworld.com/article/2073649/why-extends-is-evil.html

www.yegor256.com/2016/09/13/inheritance-is- procedural.html

refactoring.guru/ru/design-patterns/factory-method/java/example

scg.unibe.ch/archive/papers/Scha03aTraits.pdf



All Articles