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
dan_waterworth
7,3272 gold badges36 silver badges45 bronze badges
asked Dec 5, 2011 at 13:24
2
  • 2
    Mixins are more like cross-cutting aspects than straight-up inheritence. That will probably define some use-cases for you. Commented Dec 5, 2011 at 13:52
  • 1
    Composition, anyone :) Commented Dec 6, 2011 at 8:52

5 Answers 5

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
1331 silver badge7 bronze badges
answered Dec 5, 2011 at 14:06
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
answered Dec 5, 2011 at 14:04
4
  • +1 , my favorite example of "something useful across multiple objects or class hierarchies" in Ruby is Enumerable module : ruby-doc.org/core-1.9.3/Enumerable.html Commented Dec 5, 2011 at 14:31
  • 2
    I would say that multiple inheritance can be a (not so good) alternative to mixins. Commented Dec 6, 2011 at 10:43
  • @Simon I would say that mixins can be a (not so good) alternative to multiple inheritance ;) Commented Dec 6, 2011 at 14:02
  • My horse sense told me this too. :) Commented Apr 28, 2016 at 8:33
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.

answered Dec 5, 2011 at 17:08
16
  • Definitely +1- static languages have mixins too. Commented Dec 6, 2011 at 8:08
  • Right, mixins can be done in many languages - this was not my question. Dynamic languages have certain properties which might or might not make mixins different and/or more interesting in such languages - Raynos pointed one aspect out. Furthermore, you don't point out any specific reasons why one should use the IS A concept over the mixin concept. Commented Dec 6, 2011 at 9:30
  • @MagnusWolffelt: They are the same kind of thing. "Cheese is a Dairy Product". That's the rule for IS-A. What more should I say? Commented Dec 6, 2011 at 10:51
  • My question is more about design decisions - in what situations do you want inheritance, and for what reasons? In dynamic languages I see few reasons for picking inheritance over mixins, besides what Raynos described. Performance of object creation might be one reason. Commented Dec 6, 2011 at 12:15
  • 1
    @MagnusWolffelt: Clarity. It's simply a question of clarity. IS-A and inheritance are not required. They're available so that you can clearly state a relationship. Class definitions are not required. You can write everything as flat collection of static methods in a single class definition and avoid all the ways that OO programming makes things clearer. Nothing is required. But inheritance can make things Clear. Programming is supposed to be about meaning. Hence inheritance to formalize and clarify the meaning. Commented Dec 7, 2011 at 16:26
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,
}
answered Jan 28, 2014 at 23:16
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.

answered Sep 17, 2024 at 22:57

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.