Mixins

2021-06-27

The problem is: how to share code in my project?

Inheritance is the way to reuse behaviour/logic and specialize objects. You can inherit from a base class and overwrite properties. Example:

class SayHello {
  constructor(name) {
    this.name = name
  }

  hello() {
    console.log(`Hello! I'm ${this.name}`)
  }
}

class Jim extends SayHello {
  constructor() {
    super('Jim')
  }
}

new SayHello().hello() // Hello! I'm undefined
new Jim().hello() // Hello! I'm Jim

We can compose objects by inheriting from classes. Let's say that Jim class needs to know how to cook something very well. We can inherit from the Chef class:

class Jim extends Chef {}

Now Jim can cook, but it loose the skill of saying hello. TypeScript (and JavaScript) can't inherit from more than one class at the same time. We can easily resolve this problem by letting the Chef know that it needs to know how to say hello, so:

class Chef extends SayHello {}

Nice, right? ... Wait, you didn't agree with me, right? Right? Do you see the problem here?

Re-use via inheritance is the most far-reaching and drastic - your new subclass gets the entire public interface of the superclass, plus whatever public interface it exposes on its own - as well as all the private methods and members. It’s copy and paste performed by the compiler or runtime.

David Bryant Copeland at Re-use in OO: Inheritance, Composition and Mixins.

When we don't need compose behaviour using shared state, we can just mix in methods or properties. This technique is called Mixin.

##Mixins

In TS/JS we can mix objects using the spread syntax. For example, let's rewrite SayHello class as a function constructor.

const SayHello = (name) => ({
  hello() {
    console.log(`Hello! I'm ${name}`)
  }
})

// usage
SayHello().hello() // Hello! I'm undefined

Notice the absent of the new keyword. Here we aren't creating a new object using a class constructor. But creating a new object using a Factory Function. This is a better way of creating objects because you only depend on the closure context (but you can make a messe here too.)

Jim can now be rewrite as a function constructor that has SayHello mixed in:

const Jim = () =>
  Object.freeze({
    ...SayHello('Jim')
  })

Jim().hello() // Hi! I'm Jim

The real value here is when we compose multiple properties using mixins. Now we can mix in Chef properties:

const Jim = () =>
  Object.freeze({
    ...SayHello('Jim'),
    ...Chef()
  })

Now Jim can say hello and cook very well.

##Closures vs this

If you haven't notice before, I'll show you now. In the first example using classes, if we set Jim#name inherited from SayHello class, we can make it say hello using a different name:

class Jim extends SayHello {
  constructor() {
    super('Jim')
  }
}

const jim = new Jim()
jim.name = 'Jennifer'

jim.hello() // Hello! I'm Jennifer

You just can't do this using closures in the example of mixins:

const Jim = () =>
  Object.freeze({
    ...SayHello('Jim')
  })

const jim = Jim()
jim.name = 'Jennifer'
// Error: Uncaught TypeError: Cannot add property name, object is not extensible

jim.hello() // Hello! I'm Jim

As Eric Elliot said:

Mixins are a form of object composition, where component features get mixed into a composite object so that properties of each mixin become properties of the composite object.

##Compose using dependency injection

You can solve the same problem using dependency injection but this could lead you to have to implement dependency injection containers and this leads to a increase of complexity. For example:

class Jim {
  constructor(sayHello: SayHello) {
    // how initialize this object passing "Jim"
    // as argument?
    this.sayHello = sayHello
  }

  hello() {
    this.sayHello.hello()
  }
}

new Jim(new SayHello('Jim')).hello()
// Hello! I'm Jim

Now the problem is: how to inject a dependency pre configured to Jim's class? Our dependency injection container needs to know how to create those objects.

We can resolve this problem by refactoring SayHello and permit set the name at runtime, like this:

class Jim {
  constructor(sayHello: SayHello) {
    this.sayHello = sayHello
    this.sayHello.name = 'Jim'
  }

  hello() {
    this.sayHello.hello()
  }
}

Personally, I didn't see this with good eyes mostly because this kind of complexity is unnecessary.

##Summary

  • Inheritance is a way of specialize objects while reusing behaviour from
  • Using Object.freeze we can make objects immutable
#designpattern#typescript#javascript#deno

📝 Edit this page