Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
167 views
in Technique[技术] by (71.8m points)

javascript - Refactoring legacy mixin-based class hierarchies

I'm currently working on a huge javascript project which has a huge class hierarchy and heavily uses mixins to extend functionality of base classes. Here is an example of how mixin looks like, we're using compose library to create class-like objects:

// 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);
    }
  })
})

We have the following problems with this solution:

  • Mixins heavily depend on each other - you can break something by changing mixins' order in base classes.
  • There is no easy way to ensure that you can safely include some mixin in your class, you may need to implement additional methods / include additional mixins into it.
  • Child classes have hundreds of methods because of the various mixins.
  • It's almost impossible to rewrite mixin in Flow or Typescript.

I'm looking for better plugin-like alternatives which allow to gradually refactor all existing mixins with the following requirements:

  • Ability to explicitly describe all dependencies (i.e. somehow describe that PluginA requires PluginB and PluginC).
  • Plugin should not pollute target class with its methods.
  • They should be able to somehow intercept Base class logic (like in SameXAndY).
  • Plugins should be plain js classes.

I understand that there is no "easy" answer to my question, but I would really love to hear your thought on this topic. Design pattern names, relevant blog posts, or even links to the source code will be greatly appreciated.

Cheers, Vladimir.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

I did refactor the OP's example, and it does what was ask for. Let me know if I got something wrong.

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; }
<script>(function(r){function g(a){var c="none",b;Y(a)&&(ta(a)?c=ua:(b=Z(a),t("^class\s+"+a.name+"\s+\{").test(b)?c=va:t("\([^)]*\)\s+=>\s+\(").test(b)?c=wa:z(a)&&(c=xa(a)?ya:za(a)||Aa(a)||Ba(a)?Ca:aa)));return c===aa}function C(a){return["^\[object\s+",a,"\]$"].join("")}function v(a,c){var b=Da[c];return!!b&&0<=b.indexOf(a)}function M(a,c,b){return function(){var d=arguments;c.apply(b,d);return a.apply(b,d)}}function N(a,c,b){return function(){var d=arguments,e=a.apply(b,d);c.apply(b,d);return e}}function Ea(a,c,b,d){return function(){var e=arguments;c.call(b,e,d);return a.apply(b,e)}}function Fa(a,c,b,d){return function(){var e=arguments,f=a.apply(b,e);c.call(b,e,d);return f}}function Ga(a,c,b,d){return function(){var e=arguments,f=a.apply(b,e);c.call(b,f,e,d);return f}}function Ha(a,c,b,d){return function(){var e=arguments,f;try{f=a.apply(b,e)}catch(l){c.call(b,l,e,d)}return f}}function Ia(a,c,b,d){return function(){var e=arguments,f,g;try{f=a.apply(b,e)}catch(m){g=m}c.call(b,g||f,e,d);return f}}function Ja(a,c,b,d){return function(){return c.call(b,a,c,arguments,d)}}function Ka(a,c){return function(){var b=this[a];g(b)||(b=u);this[a]=M(b,c,this)}}function La(a,c){return function(){var b=this[a];g(b)||(b=u);this[a]=N(b,c,this)}}function Ma(a,c){return function(b){var d=this[a];g(d)||(d=u);this[a]=Ea(d,c,this,b)}}function Na(a,c){return function(b){var d=this[a];g(d)||(d=u);this[a]=Fa(d,c,this,b)}}function Oa(a,c){return function(b){var d=this[a];g(d)||(d=u);this[a]=Ga(d,c,this,b)}}function Pa(a,c){return function(b){var d=this[a];g(d)||(d=u);this[a]=Ha(d,c,this,b)}}function Qa(a,c){return function(b){var d=this[a];g(d)||(d=u);this[a]=Ia(d,c,this,b)}}function Ra(a,c){return function(b){var d=this[a];g(d)||(d=u);this[a]=Ja(d,c,this,b)}}function ba(a,c,b){a.useTraits.rules.push(new A({type:"without",traitList:c,methodNameList:b}))}function ca(a,c,b,d){a.useTraits.rules.push(new A({type:"as",trait:c,methodName:b,methodAlias:d}))}function da(a){return ea(p(a)).reduce(function(a,b){q(b)&&a.push(F(b));return a},[])}function fa(){var a,c,b,d,e;if(O(this))if(d=this.valueOf(),e=d.parentLink,w(e))if(b=e.getSetup(),a=b.chainData,c=a.recentCalleeName,v(c,"without"))if(c=da(arguments),1<=c.length)a.recentCalleeName="without",ba(b,d.traitList,c);else throw new h("Not even a single <String> type was passed to 'without' as to be excluded method name.");else{a=["u2026 invalid chaining of '",c,"().without()'"].join("");if("applyBehavior"==c)throw new n([a," in case of excluding one or more certain behaviors."].join(""));throw new n(a);}else throw new k("Please do not spoof the context of 'use'.");else throw new k("Please do not spoof the context of 'apply'.");return e}function P(a,c){var b=a.getSetup();if(Sa(c,a.valueOf().traitList))b.chainData.recentCalleeName="applyTraits",b=new Q({traitList:c,parentLink:a}),a.putChild(b);else throw new h("Any trait that got passed to 'apply' needs to be registered before via 'use'.");return b}function Sa(a,c){return a.every(function(a){return 0<=c.indexOf(a)})}function Ta(){var a;a=this.getSetup();var c=this.valueOf().traitList;I(this,a,a.chainData.recentCalleeName);a=P(this,c);return fa.apply(a,arguments)}function Ua(){var a=this.getSetup(),c=this.valueOf().traitList;I(this,a,a.chainData.recentCalleeName);return P(this,c)}function ga(){var a,c,b;b=p(arguments);if(w(this))if(a=this.getSetup(),c=a.chainData,c=c.recentCalleeName,I(this,a,c),a=["u2026 invalid chaining of '",c,"().apply()'"].join(""),v(c,"apply"))if(x(b[0]))if(q(b[1]))if(v(c,"applyBehavior")){c=b[0];b=F(b[1]);a=this.getSetup();var d;if(0<=this.valueOf().traitList.indexOf(c))if(d={},c.call(d),g(d[b]))a.chainData.recentCalleeName="applyBehavior",b=new R({trait:c,methodName:b,parentLink:this}),this.putChild(b);else throw new k(["The requested '",b,"' method has not been implemented by the trait that got passed to 'apply'."].join(""));else throw new h("Any trait that got passed to 'apply' needs to be registered before via 'use'.");}else throw new n([a," in case of applying just a certain behavior."].join(""));else if(p(b).every(x))if(v(c,"applyTraits"))b=P(this,b);else throw new n([a," in case of applying from one or more traits."].join(""));else throw new h("'apply(u2026)' excepts either, as its 2nd delimiting argument, just a 'String' type or exclusively one or more 'Trait' and 'Function' types.");else throw new h("'apply(<Trait|Function>, u2026)' excepts as its 1st argument explicitly either a 'Trait' or a 'Function' type.");else throw new n(a);else throw new k("'use(u2026).apply(u2026)' only works within a validly chained context, please do not try spoofing the latter.");return b}function ha(){var a;a=this;if(O(a)||G(a))a=a.valueOf(),a=a.parentLink,a=ga.apply(a,arguments);else throw new k("Please do not spoof the context of 'apply'.");return a}function I(a,c,b){var d,e;a&&!c.chainData.isTerminated&&(d=a.getChild())&&(e=d.valueOf())&&("applyTraits"==b&&O(d)?ba(c,e.traitList,[]):"applyBehavior"==b&&G(d)&&(a=e.methodName,ca(c,e.trait,a,a)));c.chainData.recentCalleeName="apply"}function R(a){this.valueOf=function(){return{trait:a.trait,methodName:a.methodName,parentLink:a.parentLink}};this.toString=function(){return["ApplyLink::singleBehavior :: ",S.stringify(a)].join("")};return this}function G(a){var c;return null!=a&&(a instanceof R||g(a.as)&&g(a.after)&&g(a.before)&&g(a.valueOf)&&(c=a.valueOf())&&x(c.trait)&&q(c.methodName)&&w(c.parentLink))}function Q(a){this.valueOf=function(){return{traitList:a.traitList,parentLink:a.parentLink}};this.toString=function(){return["ApplyLink::fromTraits :: ",S.stringify(a)].join("")};return this}function O(a){var c;return null!=a&&(a instanceof Q||g(a.without)&&g(a.valueOf)&&(c=a.valueOf())&&T(c.traitList)&&w(c.parentLink))}function U(a,c){var b=null;this.getSetup=function(){return c};this.deleteChild=function(){b=null};this.putChild=function(a){b=a};this.getChild=function(){return b};this.valueOf=function(){return{traitList:p(a.traitList)}};this.toString=function(){return["UseRoot :: ",S.stringify(a)].join("")};this.apply=g

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...