Objectively Harmful

How inheritance lays a troublesome path.


Directive 1 (On The Usage Of Types)

“Program to an ‘interface’, not an ‘implementation’.” Gang of Four (Design Patterns)

What is an interface? Simply stated, an interface is a behavior set. In Go, a behavior set relates to the methods available on any given type.

type greeter interface {
    greeting(name string) string
}

What is an implementation? An implementation is expressed behavior. We can also call implementations “concretions” due to their solid/non-abstract nature. In the following case, any instance of human is a valid greeter because it implements the relevant method.

type human struct {
    name string
}

func (h human) greeting(name string) string {
    return fmt.Sprintf("Hello, %s. I'm %s.", name, h.name)
}

Using different words than The Gang of Four: Program to required behavior, not particular concretions.

The drawbacks of depending on implementations are the:

  • limitation of argument type
  • exposure of unnecessary data/behavior
  • invitation to spaghetti code
func concreteMeet(name string, h human) { // handles one implementation
    fmt.Println(h.greeting(name))
}

func polymorphicMeet(name string, g greeter) { // handles many implementations
    fmt.Println(g.greeting(name))
}

The benefits of depending on interfaces are the:

  • reusability of logic
  • encapsulation of types
  • separation of concerns

Though, it should be kept in mind that these things are not always needed or most convenient.

Directive 2 (On The Forming Of Types)

“Favor ‘object composition’ over ‘class inheritance’.” Gang of Four (Design Patterns)

What is an object? Broadly; An object is an instance of a data structure (state) with behavior.*

// data declares a simple data structure
type data struct {
    field string
}

// method prints the field contained in the structure.
func (d *data) method() {
    fmt.Println(d.field)
}

func example() {
    d := data{field: "data"} // d is an instance of "data"
    d.method()               // d is an object
}

Some argue that primitives are not objects because they do not have methods, but many primitives have behavior like +, -, ++, !, etc. Consider: Is an instance of a struct that does not have methods still an object? Sometimes objects are defined simply as any instances of data that can be interacted with. And, some would argue that objects can only exist by using “classes”. However, that only influences how a language communicates the sharing of behavior.

So, then, what is a class? Not strictly or exhaustively, a class is:

  • structure declaration (fields)
  • assignment of some or all fields
  • declaration and assignment of behavior (methods)
  • pre-assigned “magic methods” (most are reassignable)
  • declaration of taxonomic relationships/affiliations
  • declaration of implemented behavior sets

A small spoiler: Go structs only declare structure.

What is the “composition” mentioned in “object composition”?

Object
Composition

Composition is the sharing of behavior using structural organization.

And the “inheritance” in “class inheritance”?

Class
Inheritance

Inheritance is the sharing of behavior using taxonomic relationships. Note that the gradient of color is meant to signify that whatever is inherited is not necessarily clearly conveyed.

Again, using different words than The Gang of Four: When sharing behavior, favor alterations over affiliations.

To understand the benefits of sharing behavior through composition, it will help if we understand the pain points inherent to the alternatives. Let’s take a closer look at the Object-Oriented options.

Class-based Behavior Sharing

First, let’s create a couple of similar types.

Add Classes

<?
class Human {
    protected $name;
    function __construct($name) { $this->name = $name; }

    public function greeting($name) { 
        return "Hello, $name. I'm $this->name.";
    }
}

class Wolf {
    protected $freq = 1;
    function __construct($freq) { $this->freq = $freq; }

    public function greeting($_) {
        $msg = str_repeat("woof ", $this->freq);
        return trim($msg)."!";
    }
}

$a = new Human("Alice");
$b = new Wolf(3);
$username = "Dan";
echo $a->greeting($username)."\n";
echo $b->greeting($username)."\n";
  • Two classes are setup with constructors
  • Each class performs the same behavior “greeting
  • greeting takes a name and returns a formatted message.
  • The main logic constructs a human and a wolf
  • The constructed objects greet “Dan”

Output:

Add Classes

Hello, Dan. I'm Alice.
woof woof woof!

The output is as expected.


Next, let’s create and make use of an interface.

Add Interface

<?
interface greeter {
    public function greeting($str);
}

function meet($name, greeter ...$greeters) {
    foreach ($greeters as $greeter) {
        echo $greeter->greeting($name)."\n";
    }
}

class Human implements greeter {
    // ...
}

class Wolf implements greeter {
    // ...
}

$a = new Human("Alice");
$b = new Wolf(3);
meet("Dan", $a, $b);
  • Here an interface is added along with a function that leverages the interface
  • Each class must explicitly implement the interface
  • The main logic is modified to use the newly added function

Output:

Add Interface

Hello, Dan. I'm Alice.
woof woof woof!

The output is, again, as expected (and without change).


Classes affect “taxonomic groups” through “hierarchies”. Using this knowledge, let’s try sharing behavior.

Try Multiple Inheritance

<?
class Werewolf extends Human, Wolf implements greeter {
    // ...
}

$a = new Human("Alice");
$b = new Wolf(3);
$c = new Werewolf("Carlos", 1);
meet("Dan", $a, $b, $c);

If a new class wishes to extend multiple classes, it is simply not allowed in many languages. This is generally because multiple inheritance creates indirection/complexity/grief, and is typically resolved through some linearization algorithm or by the order in which types are declared. In this case, after adding the construction of a new “werewolf” to the main logic, the code is tested and fails.

Output:

Try Multiple Inheritance

PHP Parse error:  syntax error, unexpected ',', expecting '{' in file.php on line 29

Many languages do allow multiple inheritance with varying degrees of rationality.

Multiple
Inheritance

Hopefully, it’s easy to see that sharing behavior this way can get ugly fast. The darkest area is meant to emphasize higher likelihood of issues.


This is a valid approach to the same end.

Try Inheritance

<?
class Wolf extends Human {
    // ...
}

class Werewolf extends Wolf {
    function __construct($name, $freq) {
        parent::__construct($freq); Human::__construct($name);
    }
    public function greeting($name) {
        return parent::greeting($name) . " " . Human::greeting($name);
    }
}

$a = new Human("Alice");
$b = new Wolf(3);
$c = new Werewolf("Carlos", 1);
meet("Dan", $a, $b, $c);
  • The taxonomic hierarchy is set inline with wolf extending human, and werewolf extending wolf
  • The constructors of inherited classes must be called. Otherwise, prepare for fireworks
  • The greeting method of werewolf is able to access it’s direct “parent”, but not “granparent” (To reach the grandparent, the class name is used)
  • Finally, the main logic is updated with a new werewolf

Output:

Try Inheritance

Hello, Dan. I'm Alice.
woof woof woof!
woof! Hello, Dan. I'm Carlos.

While the output was successful, it’s important to see the drawback inherent to this design.


Drawback

<?
interface messager {
    public function message();
}

function talk(messager ...$messagers) {
    foreach ($messagers as $messager) {
        echo $messager->message()."\n";
    }
}

class Human implements greeter, messager {
    // ...
    public function message() { 
        return "Nice to meet you.";
    }
}

class Wolf extends Human {
    // ...
}

class Werewolf extends Wolf {
    // ...
}

meet("Dan", $a, $b, $c);
talk($a, $b, $c);
  • Here another interface is added along with a function that leverages the interface
  • Because inheritance is currently inline, only the top-level class must explicitly implement the interface
  • The main logic is modified to use the newly added function

Output:

Drawback

Hello, Dan. I'm Alice.
woof woof woof!
woof! Hello, Dan. I'm Carlos.
Nice to meet you.
Nice to meet you.
Nice to meet you.

Silent behavioral error!

While the code ran without programmatic error, there is a behavioral/semantic error. A wolf should certainly not be able to say “Nice to meet you”.


Now to implement this same thing using composion rather than inheritance.

Try Composition

<?
class Human implements greeter, messager {
    // ...
}

class Wolf implements greeter {
    // ...
}

class Werewolf implements greeter, messager {
    protected $human;
    protected $wolf;

    function __construct($name, $freq) {
        $this->human = new Human($name);
        $this->wolf = new Wolf($freq);
    }

    public function greeting($name) {
        return $this->wolf->greeting($name) . " " 
             . $this->human->greeting($name);
    }

    public function message() {
        return $this->human->message();
    }
}

meet("Dan", $a, $b, $c);
talk($a, $c);
  • The extends keyword is dropped and inheritance is abandoned
  • Each class declares it’s own interface implementations
  • werewolf will use fields to hold full instances of human and wolf
  • With a bit of work, any needed behavior is accessed with clarity
  • The main logic is updated with the wolf being removed from the talk function (otherwise it will error programmatically)

Output:

Try Composition

Hello, Dan. I'm Alice.
woof woof woof!
woof! Hello, Dan. I'm Carlos.
Nice to meet you.
Nice to meet you.

The output is now as expected, and the solution is technically using best practices.

Prototype-based Behavior Sharing

Add Constructors

function Human(name) {
  this.name = name;

  this.greeting = function(name) {
    return "Hello, "+name+". I'm " + this.name + ".";
  };
}

function Wolf(freq) {
  this.freq = freq;

  this.greeting = function() {
    msg = "woof ".repeat(this.freq).trim();
    return msg + "!"
  };
}

a = new Human("Alice");
b = new Wolf(3);
username = "Dan";
console.log(a.greeting(username));
console.log(b.greeting(username));
  • Two object constructors are created
  • Each object is capable of the same behavior “greeting” (no difference from the class-based examples)
  • The main logic constructs a human and a wolf
  • The constructed objects greet “Dan”

Output:

Add Constructors

Hello, Dan. I'm Alice.
woof woof woof!

The output is as expected.


Next, let’s create and make use of an interface (sort of).

Add Interface

function meet(name, ...greeters) {
  for (var i = 0; i < greeters.length; i++) {
    console.log(greeters[i].greeting(name));
  };
}

a = new Human("Alice");
b = new Wolf(3);
meet("Dan", a, b);
  • While there are no interfaces in JS, a function that requires an object with a single method is added
  • The main logic is modified to use the newly added function

Output:

Add Interface

Hello, Dan. I'm Alice.
woof woof woof!

The output is, again, as expected (without change).


How will sharing behavior work out here?

Try Multiple Inheritance

function Werewolf(name, freq) {
  var human = new Human(name);
  var wolf = new Wolf(freq);

  Object.setPrototypeOf(this, human, wolf);
}

a = new Human("Alice");
b = new Wolf(3);
c = new Werewolf("Carlos", 1);
meet("Dan", a, b, c);
console.log("werewolf freq:", c.freq);

Prototype-based languages employ the concept of template objects. An object can have a defined “prototype” which is used for any behavior not provided by itself. What this means is that there are no taxonomic groupings, but there are still taxonomic hierarchies. While this is still highly limited (and can be quite fragile), it is far more structural than classes. Regardless…

  • Multiple inheritance is not permitted and setPrototypeOf silently fails as will be seen by the werewolf’s freq field not being set
  • This also means that defining the intended version of werewolf’s greeting would result in a programmatic failure, so it has been skipped
  • After adding the construction of a new werewolf to the main logic, the code can be tested

Output:

Try Multiple Inheritance

Hello, Dan. I'm Alice.
woof woof woof!
Hello, Dan. I'm Carlos.
werewolf freq: undefined

Silent behavioral error (and likely eventual programmatic failure)!


Inlining inheritance again…

Try Inheritance

function Werewolf(name, freq) {
  var human = new Human(name);
  var wolf = new Wolf(freq);

  Object.setPrototypeOf(wolf, human);
  Object.setPrototypeOf(this, wolf);

  this.greeting = function(name) {
    var parent = Object.getPrototypeOf(this);
    var grandp = Object.getPrototypeOf(parent);
    return parent.greeting(name) + " " + grandp.greeting(name);
  };
}

a = new Human("Alice");
b = new Wolf(3);
c = new Werewolf("Carlos", 1);
meet("Dan", a, b, c);
  • The taxonomic hierarchy is set inline with human acting as wolf’s prototype, and wolf acting as werewolf’s prototype
  • All ancestors are available with prototypal inheritance, but keeping to the type system is cumbersome
  • The main logic is updated with a new werewolf

Output:

Try Inheritance

Hello, Dan. I'm Alice.
woof woof woof!
woof! Hello, Dan. I'm Carlos.

Success. Because prototypal inheritance is more structural than class inheritance, the basic instance of “wolf” does not incorrectly inherit any behavior from “human”, but “werewolf” does get it “passed-through” it’s prototype “wolf”.


Approaching this now witrh composition…

Composition

function Werewolf(name, freq) {
  this.human = new Human(name);
  this.wolf = new Wolf(freq);

  Object.assign(this, this.human, this.wolf);

  this.greeting = function(name) {
    return this.wolf.greeting(name) + " " + this.human.greeting(name);
  };
}
meet("Dan", a, b, c);
talk(a, c);
  • Fields are set with instances of human and wolf
  • Object.assign is used to “apply” behavior and structure to werewolf
  • The talk function is behaviorally identical to what was used in the class-based example code

Output:

Composition

Hello, Dan. I'm Alice.
woof woof woof!
woof! Hello, Dan. I'm Carlos.
Nice to meet you.
Nice to meet you.

Despite being a little magical, accessing behavior is now more free and clear (almost).


Here’s the caveat:

Drawback

function Werewolf(name) {
  this.human = new Human(name);

  Object.assign(this, this.human);

  this.wrappedGreeting = function(name) {
    return this.human.greeting(name);
  };
}

c = new Werewolf("Carlos");

console.log("update 'human' name to 'Charlie' - call assigned, then wrapped");
c.human.name = "Charlie";
meet("Erin", c);
wrappedMeet("Frank", c);

console.log("update 'werewolf' name to 'Charles' - call assigned, then wrapped");
c.name = "Charles";
meet("Grace", c);
wrappedMeet("Heidi", c);