重构基于mixin的传统类层次结构

时间:2017-03-26 10:05:30

标签: javascript design-patterns traits mixins composition

我目前正在开发一个庞大的javascript项目,该项目具有庞大的类层次结构,并且大量使用mixins来扩展基类的功能。以下是mixin的示例,我们使用compose library创建类似对象:

// Base.js
var Base = compose({
  setX: function (x) {
    this.x = x;
  },

  setY: function (y) {
    this.y = y;
  },

  setPosition: function (x, y) {
    this.setX(x);
    this.setY(y);
  }
})

// SameXAndY.js - mixin
var SameXAndY = compose({
  // Executes after setX in Base.js
  setX: compose.after(function (x) {
    this.y = x;
  }),

  // Executes after setY in Base.js
  setY: compose.after(function (y) {
    this.x = y;
  }),

  // Overrides setPosition in Base.js
  setPosition: compose.around(function (base) {
    return function (x, y) {
      if (x !== y) {
        throw 'x !== y';
      }
      return base.call(this, x, y);
    }
  })
})

此解决方案存在以下问题:

  • Mixins在很大程度上依赖于彼此 - 你可以通过改变mixins来解决问题。在基类中订购。
  • 没有简单的方法可以确保您可以在课堂上安全地添加一些mixin,您可能需要实现其他方法/包含额外的mixin。
  • 由于各种混音,子类有数百种方法。
  • 在Flow或Typescript中重写mixin几乎是不可能的。

我正在寻找更好的类似插件的替代品,这些替代品可以根据以下要求逐步重构所有现有的mixin:

  • 能够明确描述所有依赖关系(即以某种方式描述PluginA需要PluginBPluginC)。
  • 插件不应该用其方法污染目标类。
  • 他们应该能够以某种方式拦截基类逻辑(如SameXAndY)。
  • 插件应该是普通的js类。

我知道没有" easy"回答我的问题,但我真的很想听听你对这个话题的看法。设计模式名称,相关博客文章,甚至链接到源代码将不胜感激。

干杯, 弗拉基米尔。

1 个答案:

答案 0 :(得分:1)

我确实重构了OP的例子,它做了所要求的。 如果我出错了,请告诉我。

function loop(max, callback, i) {
  i = i || 0;
  if (i < max) {
    console.log("loop count: " + i);
    return callback(i).then(function() {
      return loop(max, callback, ++i);
    });
  }
}

function doAsyncStuff(passedNum) {
  return new Promise(function(resolve, reject) {
    // Fake asynchronous stuff for 500ms using setTimeout.
    // Put your real async code here, calling "resolve" when
    // it's finished.
    setTimeout(function() {
      console.log("callback count: " + passedNum);
      resolve();
    }, 500);
  });
}

loop(5, doAsyncStuff).then(function() {
  console.log('All done looping!')
});
class BaseXYType {

  constructor(stateValue) { // injected state object.

    this.setX = function setX (x) {
      return (stateValue.x = x);
    };
    this.setY = function setY (y) {
      return (stateValue.y = y);
    };

    Object.defineProperty(this, "x", {
      get: function getX () {
        return stateValue.x;
      },
      enumerable: true
    });
    Object.defineProperty(this, "y", {
      get: function getY () {
        return stateValue.y;
      },
      enumerable: true
    });

    Object.defineProperty(this, 'valueOf', {
      value: function valueOf () {
        return Object.assign({}, stateValue);
      }
    });
    Object.defineProperty(this, 'toString', {
      value: function toString () {
        return JSON.stringify(stateValue);
      }
    });
  }

  setPosition(x, y) { // prototypal method.
    this.setX(x);
    this.setY(y);
    return this.valueOf();
  }
}


class SameXYType extends BaseXYType {

  // - Traits in JavaScript should be applicable types/objects that are
  //   just containers of trait and object composition rules which
  //   exclusively will be executed at a trait's apply time.

  constructor(stateValue) { // injected state object.
    super(stateValue);

    withSameInternalXAndYState.call(this, stateValue);
  }
}


var withSameInternalXAndYState = Trait.create(function (use, applicator) {

  // local functions in order to enable shared code, thus achieving less memory consumption.
  //
  function afterReturningStateChangeXHandler(returnValue, argsArray, payloadList) {
    var
      stateValue = payloadList[0];

    stateValue.y = argsArray[0];  // same y from x.
  }
  function afterReturningStateChangeYHandler(returnValue, argsArray, payloadList) {
    var
      stateValue = payloadList[0];

    stateValue.x = argsArray[0];  // same x from y.
  }
  function setPositionInterceptor(proceedSetPosition, interceptor, argsArray, payloadList) {
    var
      x = argsArray[0],
      y = argsArray[1];

    if (x !== y) {
      throw (new TypeError([x, "!==", y].join(" ")));
    }
    return proceedSetPosition.call(this, x, y);
  }

  applicator(function sameXAndYBehavior (stateValue) {

    // no additional trait specific behavior within the applicator

  }).requires([

      "setX",
      "setY",
      "setPosition"

  ]).afterReturning(

    "setX", afterReturningStateChangeXHandler

  ).afterReturning(

    "setY", afterReturningStateChangeYHandler

  ).around(

    "setPosition", setPositionInterceptor
  );
});


var
  base_1 = new BaseXYType({ x: 7, y: 11 }),
  base_2 = new BaseXYType({ x: 99, y: 1 }),

  same_1 = new SameXYType({ x: 13, y: 5 }),
  same_2 = new SameXYType({ x: 99, y: 1 });


console.log('("" + base_1) : ', ("" + base_1));
console.log('("" + base_2) : ', ("" + base_2));
console.log('("" + same_1) : ', ("" + same_1));
console.log('("" + same_2) : ', ("" + same_2));

console.log('base_1.valueOf() : ', base_1.valueOf());
console.log('base_2.valueOf() : ', base_2.valueOf());
console.log('same_1.valueOf() : ', same_1.valueOf());
console.log('same_2.valueOf() : ', same_2.valueOf());


console.log('base_1.x : ', base_1.x);
console.log('(base_1.x = "foo") : ', (base_1.x = "foo"));
console.log('base_1.x : ', base_1.x);

console.log('base_1.y : ', base_1.y);
console.log('(base_1.y = "bar") : ', (base_1.y = "bar"));
console.log('base_1.y : ', base_1.y);

console.log('same_2.x : ', same_2.x);
console.log('(same_2.x = "biz") : ', (same_2.x = "biz"));
console.log('same_2.x : ', same_2.x);

console.log('same_2.y : ', same_2.y);
console.log('(same_2.y = "baz") : ', (same_2.y = "baz"));
console.log('same_2.y : ', same_2.y);


console.log('base_1.setY("foo") : ', base_1.setY("foo"));
console.log('base_1.y : ', base_1.y);

console.log('base_2.setX("bar") : ', base_2.setX("bar"));
console.log('base_2.x : ', base_2.x);


console.log('base_1.setPosition("brown", "fox") : ', base_1.setPosition("brown", "fox"));
console.log('("" + base_1) : ', ("" + base_1));

console.log('base_2.setPosition("lazy", "dog") : ', base_2.setPosition("lazy", "dog"));
console.log('("" + base_2) : ', ("" + base_2));


console.log('same_1.setY(543) : ', same_1.setY(543));
console.log('same_1.x : ', same_1.x);
console.log('same_1.y : ', same_1.y);
console.log('same_1.valueOf() : ', same_1.valueOf());

console.log('same_2.setY(79) : ', same_2.setY(79));
console.log('same_2.x : ', same_2.x);
console.log('same_2.y : ', same_2.y);
console.log('same_2.valueOf() : ', same_2.valueOf());


console.log('same_1.setPosition(77, 77) : ', same_1.setPosition(77, 77));
console.log('("" + same_1) : ', ("" + same_1));

console.log('same_2.setPosition(42, 42") : ', same_2.setPosition(42, 42));
console.log('("" + same_2) : ', ("" + same_2));


console.log('same_1.setPosition("apple", "pear") : ', same_1.setPosition("apple", "pear"));
console.log('("" + same_1) : ', ("" + same_1));

console.log('same_1.setPosition("apple", "pear") : ', same_1.setPosition("prune", "prune"));
console.log('("" + same_1) : ', ("" + same_1));
.as-console-wrapper { max-height: 100%!important; top: 0; }