UIZE JavaScript Framework

WHITE PAPERS Javascript Mixins

1. Introduction

The UIZE JavaScript Framework provides a versatile mixin system for implementing reusable fragments of functionality that can be mixed in to different classes in a class hierarchy.

1.1. PROPOSAL

This document is a proposal for a feature and is written in the style of describing the feature as though it were already implemented (i.e. documentation for a future feature).

2. Mixins as Augmenter Modules

As implemented in the UIZE JavaScript Framework, mixins are essentially augmenter modules.

A mixin module is intended to augment other modules by adding new caoabilities or modifying their existing capabilities. An example of a mixin module would be a module that adds an area state property to a Rectangle class, where the value of the area state property is derived from the values of the Rectangle class' existing width and height state properties.

2.1. Mixins Need to be Applied

As augmenter modules, mixins need to be loaded and then deliberately applied to the desired modules.

This can be done in a few different ways, to suit your situation and tastes...

2.1.1. Declared as a Feature When Subclassing

.

EXAMPLE

var MyNamespace.MyClass = Uize.Class.subclass ({
  mixins:MyNamespace.MyAwesomeMixin
});

2.1.2. Declared as a Feature on an Existing Class

.

EXAMPLE

var MyNamespace.MyClass = Uize.Class.subclass ();

MyNamespace.MyClass.mixin (MyNamespace.MyAwesomeMixin);

2.1.3. Mixed in Using the Uize.mixin Method

.

EXAMPLE

var MyNamespace.MyClass = Uize.Class.subclass ();

Uize.mixin (MyNamespace.MyClass,MyNamespace.MyAwesomeMixin);

2.2. Loading Mixins Doesn't Have Side Effects

Because mixins need to be applied deliberately in order for the mixin functionality to be added to other modues, simply loading mixins doesn't have side effects.

2.2.1. Mixin Modules Versus Extension Modules

.

This is in stark contrast to extension modules, that extend specific modules when they are loaded. Extension modules are less versatile because they are only designed to augment specific modules,

2.3. Mixins Aren't Standalones

.

Mixins are really reusable fragments of functionality that are intended to be used by multiple other modules in order to "mix in" capabilities that need to be available in all those other modules, but where that effect cannot be accomplished through a standard inheritance approach. The different modules may be classes from entirely different class hierarchies that do not share a common base class that can be modified)

2.4. Classes Can't be Mixins

.

3. The Uize.mixin Method

.

DIFFERENT USAGES

Mix in a Set of Features, by Specifying an Augmenter Function

sourceFUNCorOBJ = Uize.mixin (sourceFUNCorOBJ,mixinFUNC);

Mix in a Set of Features, by Specifying a Features Object

sourceFUNCorOBJ = Uize.mixin (sourceFUNCorOBJ,mixinOBJ);

Mix in Multiple Mixins, by Specifying an Array of Mixins

sourceFUNCorOBJ = Uize.mixin (sourceFUNCorOBJ,mixinsARRAY);

Mix in One or More Mixins, Specifying an Optional Mixer Function

sourceFUNCorOBJ = Uize.mixin (FUNCorOBJ,mixinOrMixinsFUNCorOBJorARRAY);

3.1. Implementation Approach

PSEUDOCODE

if (not mixin has already been applied) {
  if (mixinOrMixins is a function) {
    call it with the source as an argument
  } else if (mixinOrMixins is an array) {
    for (each element of the mixinOrMixins array) {
      call Uize.mixin with source as first argument and element as second argument
    }
  } else {
    if (a mixer method is provided) {
      call mixer method with source as first argument and mixinOrMixins as second argument
    } else {
      merge the mixinOrMixins object into the source
    }
  }
}

IMPLEMENTATION

Uize.mixin = function (_source,_mixin,_mixer) {
  function _mixIntoSource (_mixin) {
    Uize.isIn (_source.appliedMixins,_mixin)
      ? 0
      : Uize.isFunction (_mixin)
        /* TODO:
          What about the case where the mixin is a class, so that its type would be a function? Perhaps we should check for a method that performs the mixin of the features from the class, and where this method could be implemented in the Uize.Class base class with a base implementation that mixes in all the static and instance features.
        */
        ? _mixin (_source)
        : Uize.isArray (_mixin)
          ? Uize.forEach (_mixin,_mixIntoSource)
          : _mixer
            ? _mixer (_source,_mixin)
            : Uize.mergeInto (_source,_mixin)
    ;
  }
  _mixIntoSource (_mixin);
  return _source;
};

Uize.Class.mixins = function (_mixin) {
  return Uize.mixin (this,_mixin,function (_source,_mixin) {_source.declare (_mixin)});
};
Uize.Class.mixins = function (_source,_mixin) {
  function _mixIntoSource (_mixin) {
    if (!Uize.isIn (_source.appliedMixins,_mixin)) {
      _source.appliedMixins.push (_mixin);
      Uize.isFunction (_mixin)
        /* TODO:
          What about the case where the mixin is a class, so that its type would be a function? Perhaps we should check for a method that performs the mixin of the features from the class, and where this method could be implemented in the Uize.Class base class with a base implementation that mixes in all the static and instance features.
        */
        ? _mixin (_source)
        : Uize.isArray (_mixin)
          ? Uize.forEach (_mixin,_mixIntoSource)
          : _source.declare (_mixin)
    }
  }
  _mixIntoSource (_mixin);
};

4. Class Mixins

.

4.1. Defining Class Mixin Modules

.

4.1.1. A Class Mixin as a Features Object

.

EXAMPLE

Uize.module ({
  name:'MyNamespace.WidthAndHeightMixin',
  builder:function () {
    return {
      stateProperties:{
        width:{value:0},
        height:{value:0}
      }
    };
  }
});

4.1.2. A Class Mixin as a Class Augmenter

When a class mixin is implemented as a class augmenter, the module simply defines an augmenter function that will be called and passed a reference to the class that is to be augmented as the single argument.

The augmenter function can then call any of the feature declaration methods on the class to add features to it or override features already defined for the class.

EXAMPLE

Uize.module ({
  name:'MyNamespace.WidthAndHeightMixin',
  builder:function () {
    return function (_class) {
      _class.stateProperties ({
        width:{value:0},
        height:{value:0}
      });
    };
  }
});

4.1.3. When to Use a Class Augmenter

.

4.2. Using Class Mixin Modules

.

4.2.1. The MyClass.mixin Static Method

The MyClass.mixin feature declaration method allows one to specify one or more mixins that should be applied to the class.

EXAMPLE

_class.mixin (MyNamespace.MyMixin);

4.2.1.1. Declaring Mixins During Subclassing

As with any of the feature declaration methods, the mixins can also be declared in the features object when calling the MyClass.subclass static method to create a new subclass.

4.2.1.1.1. Using a Single Class Mixin Module

.

EXAMPLE

Uize.module ({
  name:'MyNamespace.Rect',
  superclass:'Uize.Class',
  required:'MyNamespace.WidthAndHeightMixin',
  builder:function (_superclass) {
    return _superclass.subclass ({
      mixins:MyNamespace.WidthAndHeightMixin
    });
  }
});
4.2.1.1.2. Using Multiple Class Mixin Modules

.

EXAMPLE

Uize.module ({
  name:'MyNamespace.Rect',
  superclass:'Uize.Class',
  required:[
    'MyNamespace.WidthAndHeightMixin',
    'MyNamespace.AreaMixin'
  ],
  builder:function (_superclass) {
    return _superclass.subclass ({
      mixins:[
        MyNamespace.WidthAndHeightMixin,
        MyNamespace.AreaMixin
      ]
    });
  }
});

4.2.1.2. Applying a Mixin to an Existing Class

.

EXAMPLE

MyNamespace.Rect.mixin (MyNamespace.WidthAndHeightMixin);
4.2.1.2.1. Applying a Mixin Using the MyClass.mixin Static Method

.

EXAMPLE

Uize.module ({
  name:'MyNamespace.Rect',
  superclass:'Uize.Class',
  required:'MyNamespace.WidthAndHeightMixin',
  builder:function (_superclass) {
    var _class = _superclass.subclass ();
    // ... ... ... ...

    _class.mixin (MyNamespace.WidthAndHeightMixin);

    // ... ... ... ...
    return _class;
  }
});
4.2.1.2.2. Applying a Mixin Using the Uize.mixin Static Method

.

EXAMPLE

Uize.module ({
  name:'MyNamespace.Rect',
  superclass:'Uize.Class',
  required:'MyNamespace.WidthAndHeightMixin',
  builder:function (_superclass) {
    var _class = _superclass.subclass ();
    // ... ... ... ...

    Uize.mixin (_class,MyNamespace.WidthAndHeightMixin);

    // ... ... ... ...
    return _class;
  }
});

4.3. Mixin Functionality Organized Into Layers

.

EXAMPLE

// a width-and-height mixin
Uize.module ({
  name:'MyNamespace.WidthAndHeightMixin',
  builder:function () {
    return {
      stateProperties:{
        width:{value:0},
        height:{value:0}
      }
    };
  }
});

// an area mixin
Uize.module ({
  name:'MyNamespace.AreaMixin',
  builder:function () {
    return {
      stateProperties:{
        area:{derived:'width,height: width * height'}
      }
    };
  }
});

// a depth mixin
Uize.module ({
  name:'MyNamespace.DepthMixin',
  builder:function () {
    return {
      stateProperties:{
        width:{depth:0}
      }
    };
  }
});

// a volume mixin
Uize.module ({
  name:'MyNamespace.VolumeMixin',
  builder:function () {
    return {
      stateProperties:{
        volume:{derived:'width,height,depth: width * height * depth'}
      }
    };
  }
});

// a rectangle class that uses the width-and-height and area mixins
Uize.module ({
  name:'MyNamespace.Rect',
  superclass:'Uize.Class',
  required:[
    'MyNamespace.WidthAndHeightMixin',
    'MyNamespace.AreaMixin'
  ],
  builder:function (_superclass) {
    return _superclass.subclass ({
      mixins:[MyNamespace.WidthAndHeightMixin,MyNamespace.AreaMixin]
    });
  }
});

// a cuboid class that subclasses the rect class and uses the depth and volume mixins
Uize.module ({
  name:'MyNamespace.Cuboid',
  superclass:'MyNamespace.Rect',
  required:[
    'MyNamespace.DepthMixin',
    'MyNamespace.VolumeMixin'
  ],
  builder:function (_superclass) {
    return _superclass.subclass ({
      mixins:[MyNamespace.DepthMixin,MyNamespace.VolumeMixin]
    });
  }
});

4.4. Aggregating Class Mixins

.

4.4.1. Aggregating Class Mixins Using the Augmenter Approach

.

EXAMPLE

Uize.module ({
  name:'MyNamespace.RectMixin',
  required:[
    'MyNamespace.WidthAndHeightMixin',
    'MyNamespace.AreaMixin'
  ],
  builder:function () {
    return function (_class) {
      _class.mixin ([
        MyNamespace.WidthAndHeightMixin,
        MyNamespace.AreaMixin
      ]);
    ];
  }
});

4.4.2. Aggregating Class Mixins Using the Mixins List Approach

.

EXAMPLE

Uize.module ({
  name:'MyNamespace.RectMixin',
  required:[
    'MyNamespace.WidthAndHeightMixin',
    'MyNamespace.AreaMixin'
  ],
  builder:function () {
    return [
      MyNamespace.WidthAndHeightMixin,
      MyNamespace.AreaMixin
    ];
  }
});

4.4.3. Layered Aggregation of Mixins

.

EXAMPLE

// a rect mixin
Uize.module ({
  name:'MyNamespace.RectMixin',
  required:[
    'MyNamespace.WidthAndHeightMixin',
    'MyNamespace.AreaMixin'
  ],
  builder:function () {
    return [
      MyNamespace.WidthAndHeightMixin,
      MyNamespace.AreaMixin
    ];
  }
});

// a cuboid mixin
Uize.module ({
  name:'MyNamespace.CuboidMixin',
  required:[
    'MyNamespace.RectMixin',
    'MyNamespace.DepthMixin',
    'MyNamespace.VolumeMixin'
  ],
  builder:function () {
    return [
      MyNamespace.RectMixin,
      MyNamespace.DepthMixin,
      MyNamespace.VolumeMixin
    ];
  }
});

5. Object Mixins

.

6. Mutual Compatibility

.

6.1. Compatibility Between Host and Mixin

A mixin will likely have an expectation of the host in which it is to be mixed into.

A mixin may expect to be mixed in to a specific class or subclass thereof, and may expect specific features to be present that may be used in the mixin's implementation.

6.2. Compatibility Between Mixins

.

6.3. Shared Objects and Privates

.

6.3.1. Avoid Namespace Conflicts for Privates

.

7. Considerations for Implementing Mixins

7.1. Be Careful With Private Static Properties

.

7.1.1. Be Careful with Using Module Scope Variables

.

7.2. Avoid Order Requirements Amongst Mixins

.

8. Special Design Requirements

loading a mixin should not have side effects
a mixin should be able to aggregate one or more other mixins
a mixin should be able to augment another mixin to produce a new mixin
if two mixins require a common mixin, then applying both of them should not redundantly apply the common mixin
it should be possible to determine what mixins have been applied to an object
subclasses should inherit the information about applied mixins
it should be possible to determine the mixin origin of a mixed in feature of a class
the documentation system should be able to detect the features of a mixin in order to produce generated documentation for mixin modules. Perhaps this argues in favor of making class mixins actual classes. If this approach were taken, the mixins could be implemented as classes that also implement a special method for mixing in their features into other classes (which would also be used internally in the implementation of the classes). Some alternative approaches: 1) the process of declaring features for a class could result in the features being stored in such a way that they could be declared again for a class into which they are to be mixed, 2) the entire module builder for a mixin could be applied, by supplying the mixin host as the value for the _superclass variable of the builder.

9. Open Questions

9.1. Can Mixins be Standalone?

For example, if two widget classes existed that implemented some part of a UI, would it be possible to create a new subclass and then mixin those two widget classes?

An issue with mixing in standalone classes is that one can perform duplicate / redundant / wasted effort in copying in the guts of the classes to be mixed in. Ideally, if one created a new widget class and wanted to mix in the features of two other widget classes, only the modifications made in those widget classes to the features inherited from the widget base class would be copied over to the class into which the classes are being mixed in.

So, if widget A, widget B, and widget C all subclassed Uize.Widget, then mixing in widget B and widget C into widget A would only mix in the feature deltas between Uize.Widget and widget B and Uize.Widget and widget C. In order to achieve this, the mixin logic would need to trace up the inheritance chain and find the common base class for all the classes involved.

In reality, it may be more practical to require mixins to be implemented in a specific way - not as standalone classes but as class augmenters that have a specific expectation of the classes that they are to augment.

9.2. Determining Where Features are Introduced

With an implementation that treats a mixin as an augmenter, there is no way to programmatically determine that a feature that is mixed into a class actually originates from a mixin.

To a tool like the DELVE tool, feature discovery will attribute mixed in methods to the classes in the inheritance chain into which they were mixed in - not the mixin modules in which they are implemented. The only way to overcome this would be to tag the functions with the modules in which they are implemented. This would involve somehow providing the mixin module also as a reference when declaring features, but it is unclear how this reference would be passed. Additionally, tagging as can be done with function references cannot be accomplished with instance properties, static properties, or state properties.

All of this suggests that features should be declared in a more formalized manner and that classes on which features are declared should possibly maintain a registry of the features and where they were introduced and overridden.