20

When should you prefer inheritance patterns over mixins in dynamic languages?

By mixins, I mean actual proper mixing in, as in inserting functions and data members into an object in runtime.

When would you use, for example, prototypal inheritance instead of mixins? To illustrate more clearly what I mean by mixin, some pseudocode:

asCircle(obj) {
  obj.radius = 0
  obj.area = function() {
    return this.radius * this.radius * 3.14
  }

myObject = {}
asCircle(myObject)
myObject.area() // -> 0

5 Answers5

14

Prototypical inheritance is simple. It has a single advantage over mixins.

That is that it's a live link. if you change the prototype everything that inherits it is changed.

Example using pd

var Circle = {
  constructor: function _constructor() {
    this.radius = 0;
    return this;
  },
  area: function _area() {
    return this.radius * this.radius * Circle.PI
  },
  PI: 3.14
};

var mixedIn = pd.extend({}, Circle).constructor();
var inherited = pd.make(Circle, {}).constructor();

Circle.perimeter = perimeter;

inherited.perimeter(); // wins
mixedIn.perimeter(); // fails

function perimeter() {
  return 2 * this.radius;
}

So basically, if you want changes to the "interface" Circle to reflect at run-time to all objects that "use" it's functionality, then inherit from it.

If you do not want changes to reflect then mix it in.

Note that mixins have more purpose than that as well. Mixins are your mechanism for multiple "inheritance".

If you want an object to implement multiple "interfaces" then you will have to mix some in. The one you use for prototypical inheritance is the one you want changes to reflect for at run-time, the others will be mixed in.

Zak
  • 133
  • 1
  • 7
Raynos
  • 8,590
13

My horse sense tells me this:

  • If something is useful across multiple objects or class hierarchies -- make it a mixin
  • If something is only useful along a single hierarchy -- use inheritance

Related Notes:

  • The word "useful" should be taken metaphorically
  • For those languages that don't have multiple inheritance, mixins are a good alternative
  • PHP 5.4 introduces traits that have goodness from both mixins and multiple inheritance worlds
treecoder
  • 9,495
10

Use the "Is-a" test.

Inheritance is limited to the case when you can say "Subclass IS A Superclass". They are the same kind of thing. "Cheese is a Dairy Product".

Mixins are for everything else. "Cheese can be used in a sandwich". Cheese isn't a sandwich, but it participates in sandwiching.

PS. This has nothing to do with dynamic languages. Any multiple inheritance language with static compilation (i.e., C++) has the same decision point.

S.Lott
  • 45,522
  • 6
  • 93
  • 155
0

Well, the best example I can give it to you is an Actor for a game which has inheritance for some base stuff but uses mixins/plugins for shared functionality. The shared functionality could be (directly from the source code!):

var plugins = {
    SingleVisualEntity : SingleVisualEntity,
    JumpBehaviour      : JumpBehaviour,
    WeaponBehaviour    : WeaponBehaviour,
    RadarBehaviour     : RadarBehaviour,
    EnergyGatherer     : EnergyGatherer,
    LifeBarPlugin      : LifeBarPlugin,
    SelectionPlugin    : SelectionPlugin,
    UpgradePlugin      : UpgradePlugin,
    BrainPlugin        : BrainPlugin,
    PlanetObjectPlugin : PlanetObjectPlugin,
}
Totty.js
  • 170
0

When should you prefer inheritance?

Like S.Lotti said above when it has an clear and unambigous IS-A relationship with its parent. It's just that the explanation he gave you lacks an example:

Why model it as an IS-A relationship?

Because it implies strong behavioral subtyping which means you can treat all it's subtypes as substitutable, therefore you can safely assume you can treat them the same, i.e:

// - assume Cat, Dog & Bird 
//   are subtypes of Animal.
// - they just override `speak` without changing call signature

const animals = [cat, dog, bird]

animals.forEach(animal => animal.speak())

I could have any of those 3 instances be the animal in forEach and it wouldn't produce an error or cause them to speak something incorrectly. Because I made sure to model the relationships in a way that's safe.

This is Liskov Substitution. It's applicable to both static and dynamic languages because behavioral subtyping cannot be checked by a compiler.

So if it semantically makes sense, you need all the methods of the parent and also satisfies Liskov then it is a good candidate for strong behavioral subtyping, which can be expressed via an IS-A relationship

The concepts of subclassing via inheritance and subtyping are not one and the same AFAIK; but the intent to express such a relationship is via inheritance.

Inheritance is the strongest coupling relationship. Often times too strong, inflexibly so.

What you're expressing is that the child class must have an is-a relationship with the parent and what you're implying is that type: child is a subset of type: parent, therefore the parent can be substituted with the child without the potential for crashes or incorrect behaviour.

When an is-a relationship is established, it's expected that you should be able to treat all subtypes uniformly, through their polymorphic interface without the potential for issues; otherwise an is-a relationship is incorrect.

The classroom example of a Liskov Violation is the Circle/Ellipse problem:

Example:

  • I have an Ellipse class
  • I assume a Circle is an Ellipse with a fixed rx/ry so I model it as: Circle extends Ellipse
class Ellipse {
  constructor(rx, ry) {
    this.rx = rx
    this.ry = ry
  }

resize(rx, ry) { this.rx = rx this.ry = ry

return this

} }

class Circle extends Ellipse { constructor(radius) { super(radius, radius) }

resize(radius) { this.resizeWidth(radius) this.resizeHeight(radius)

return this

} }

// not really relevant, // assume they are implemented class Rectangle {} class Triangle {}

Now suppose I have this list of shapes:

const shapes = [
  new Ellipse(20, 30),
  new Rectangle(20, 10),
  new Triangle(10, 5, 10),
  new Circle(10), 
  new Ellipse(20, 20)
]

I decide to resize all Circles:

shapes
  .filter(shape => shape instanceof Circle)
  .map(circle => circle.resize(50))

// Circle rx: 50, ry: 50

All good, no issues here.

But if I were to attempt a method on the parent:

shapes
  .filter(shape => shape instanceof Ellipse)
  .map(ellipse => ellipse.resizeWidth(50))

// Ellipse rx: 50, ry: 30 // Circle rx: 50, ry: 10 <-- oops // Ellipse rx: 50, ry: 20

ok, so it didn't throw an Error.

... but the Circle was stretched horizontally; therefore it's no longer a Circle. It's invariant was violated and the program is now incorrect.

All I've done is a run-of-the-mill call of a method assumed to be polymorphic. So an is-a relationship is not correct, even though it looks very appropriate. A different & more complex hierarchy needs to be built if you need to squeeze as much code reuse & as much polymorphism as possible.

Mixins allow attaching behaviour without creating this is-a relationship.

They allow breaking up the parent type into multiple, smaller & more specific parent types from which you can selectively subtype, so the above principle isn't violated.

For example:

  • Circle: extends Shape & RadialShape

  • Ellipse: extends Shape & RadialShape

Note that you can make sure the Liskov principle isn't violated by simply avoid subtyping altogether.

But that means you lost the ability of treating all subsets of a type in a uniform method. So you have to find the right design that maximizes code reuse and correct & safe type relations.

I think mixins are just a type of than composition, usually found in languages that don't support multiple inheritance and need to emulate it.

nicholaswmin
  • 2,019