Objectively Harmful

daved

introtype systeminheritance

3292 Words

2019-04-03 20:36 +0000


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 composition 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. Or is it? (It probably is.)


Verifying how inheritance behaves in JS…

Prove It
function talk(...messagers) {
  for (var i = 0; i < messagers.length; i++) {
    console.log(messagers[i].message())
  }
}

function Human(name) {
  // ...
  this.message = function() {
    return "Nice to meet you.";
  }
}

meet("Dan", a, b, c);
talk(a, c);
  • Another function that requires an object with a single method is added
  • This inheritance is inline, so only the top-level object must have the new method added
  • The main logic is modified to use the newly added function

Output:

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

Because prototypal inheritance is more structural than class inheritance, inlining prototypes will only affect the defining types. In this case, all “werewolf” objects can “talk”, but no “wolf” object can.


Approaching this now with 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);
  • composing only a “human”; the “human” is assigned to the “werewolf” which now has the “greeting” function from the “human” — the “werewolf” also is given a “wrappedGreeting” function
  • in the main logic, the “werewolf” is created, then the “human” “name” is updated, then the assigned and wrapped functions are called. the “werewolf” “name” is updated, then the assigned and wrapped functions are called
  • what names should be expected? using composition, the “human” “greeting” function should only access “human” fields — so, the output should show “charlie, charlie, charlie, charlie”.

Prototype-based Composition (drawback) Output

.code ./code/js/g_composition_drawback.js.out

the “greeting” function assigned from “human” to “werewolf” accesses the “werewolf” “name” field ignoring the “human” “name” field

however, the wrapped “greeting” function accesses the “human” “name” field
this inconsistency can be somewhat convenient to abuse, but it is leaky

Prototype-based Composition

.code ./code/js/h_composition.js /BGN1/,/END1/

.code ./code/js/h_composition.js /BGN2/,/END2/

the only notable change here is that the function “bind” is used to ensure that the “human” “greeting” function is always bound to it’s own scope

Prototype-based Composition Output

.code ./code/js/h_composition.js.out

success

now the “human” “greeting” function always uses the “human” “name” field
this means that it cannot access or affect “werewolf”-level fields
while this may seem like an undue limitation, it is correct behavior for composed types - that is… this code is no longer leaky

Object-based Behavior Sharing

Object-based Example

.code ./code/go/a_structs.go /BGN1/,/END1/

two structs are created (no construction functions at this point)

each struct provides the same behavior “greeting”
the main logic constructs a “human” and a “wolf”, then has them meet “Dan”

Object-based Example Output

.code ./code/go/a_structs.go.out

the output is as expected

Object-based w/Interface

.code ./code/go/b_interface.go /BGN1/,/END1/

here an interface is added along with a function that leverages the interface

interfaces are implicity satisfied in Go, so nothing more is needed
the main logic is modified to use the newly added function

Object-based w/Interface Output

.code ./code/go/b_interface.go.out

the output, again, is as expected (without change)

Object-based Composition (incomplete)

.code ./code/go/c_composition_incomplete.go /BGN1/,/END1/

skipping the non-existent inheritance…

a new struct is created which embeds a “human” and a “wolf”
behavior sharing is immediately structurally facilitated due to embedded methods and fields being “promoted” to top-level
a construction function is added that handles the construction of the “human” and “wolf” dependencies
after adding the construction of a new “werewolf” to the main logic, the code can be tested

Object-based Composition (incomplete) Output

.code ./code/go/c_composition_incomplete.go.out

at compile time it is clear that ambiguity must be resolved

Object-based Composition (working)

.code ./code/go/d_composition.go /BGN1/,/END1/

because Go does not declare any taxonomic relationships (nor any relationship rules), ambiguities are forced to the forefront

“spooky action at a distance” is avoided for methods
be careful with fields and their related scopes
the ambiguity is resolved by defining a method directly on the embedding type

Object-based Composition (working) Output

.code ./code/go/d_composition.go.out

looking good

Object-based Composition (success)

.code ./code/go/e_composition_good.go /BGN1/,/END1/

.code ./code/go/e_composition_good.go /BGN2/,/END2/

.code ./code/go/e_composition_good.go /BGN3/,/END3/

here another interface is added along with a function that leverages the interface

only the “human” type needs to implement the interface
the main logic is modified to use the newly added function

Object-based Composition (success) Output

.code ./code/go/e_composition_good.go.out

success confirmed

if “bob” the “wolf” had been sent to talk, it would have been a programmatic compile time error

So What?

Composition And Interfaces Are Fundamentally Useful

Inheritance (often):

.image https://storage.euggo.org/present/img/wrong_turn_somewhere.jpg 360 _

.caption It’s even worse without strong typing!

Since 1994 our community has been formally admonished to understand this.

Encumbrances on either composition or inheritance should be considered a severe cost for the development of complex systems.

If Go Lacks Inheritance Is It Still Object-Oriented?

Absolutely. It epitomizes good OO practice.

.image https://storage.euggo.org/present/img/yes_snoop.gif 300 _

Go not only favors composition over inheritance, it foregoes inheritance entirely. Inheritance through taxonimic relationships is hostile to the stable construction of complex systems.

It could be argued that Go is only object-based and not object-oriented due to it’s lack of inheritance, but the inclusion of embedding (and the resulting field/method promotion) emulates inheritance in a structured and useful manner such that it thoroughly models the behavior sharing effectiveness of object-oriented languages while ensuring that best practices are followed.

When Might The Two Most Prominent OO Directives Not Apply?

Consider not using interfaces when dealing with:

  • optimizations
  • scripts
  • not fully understood complexity/abstractions

Consider not using composition when dealing with:

  • languages that make composition more difficult than workarounds (e.g. traits)
: interfaces have a cost, so optimization can be achieved by avoiding them
the verbosity and intellectual cost of interfaces may not be useful enough to define for small programs
designing an interface without first understanding the abstraction has it’s own costs (slowed refactoring, delayed growth, etc)
sometimes languages offer built-in workarounds that are sufficient (and maybe even quite convenient). Use them.

Are There Any Pertinent Technical Quotes?

“Orthogonality is a system design property which guarantees that modifying the technical effect produced by a component of a system neither creates nor propagates side effects to other components of the system. Typically this is achieved through the separation of concerns and encapsulation, and it is essential for feasible and compact designs of complex systems.”

.caption [[https://en.wikipedia.org/wiki/Orthogonality#Computer_science][Wikipedia - Orthogonality]]

.image https://storage.euggo.org/present/img/its_science.gif 300 _

Are There Any Pertinent Philosophical Quotes?

“It is important that we know where we come from, because if you do not know where you come from, then you don’t know where you are, and if you don’t know where you are, you don’t know where you’re going. And if you don’t know where you’re going, you’re probably going wrong.” - Terry Pratchett

.image https://storage.euggo.org/present/img/confused_travolta.gif

Can We See The Multiple Inheritance Graphic Again?

Sure:

.image ./img/svg/multi_inheritance.svg

Is There A Graphic For “Multiple Composition”?

Yep:

.image ./img/svg/multi_composition.svg

while composition shines for methods, if fields are treated as one would treat methods, there will be difficulty.

however, this is simply resolved by making all field access explicit. personally, I try to never access “promoted” fields.
when composing a type’s available methods, we are usually concerned with how some interface is being satisfied. fields are not a concern on that “side” of an interface

Are Object.assign And Object.setPrototypeOf Available In ES5?

.code ./code/f_es5_polyfill.js

sort of, and not setPrototypeOf in IE. here are the polyfills

What If An Unwanted Method Is Promoted?

.code ./code/g_hide_promoted.go /BGN1/,/END1/

.code ./code/g_hide_promoted.go.out

.caption [[https://play.golang.org/p/brnETxnsC1y][on the golang.org playground]]

Are We Finished Here?

If you’re done asking questions…

.image https://storage.euggo.org/present/img/malfunction_false.gif