How to handle inheritance from two similar sub-cla

2019-04-14 11:00发布

问题:

I've used the first two videos in this series to learn about some basic OOP concepts.

Lately, I primarily write in Node, so I'm working with prototypical inheritance on the front-end and back-end. However, these tutorials showcase OOP concepts with Java. Java is a strictly-typed language which utilizes classical inheritance.

This question pertains to both classical and prototypical inheritance, but in different ways.

This problem is a little bit difficult to put into words, so I'll use an example:

I've created a super-class called animal. I then create two sub-classes of animal: horse and donkey. Now my program requires a hybrid of the two sub-classes. Creating a mule actually seems to be a little tricky.

At first the answer seemed obvious; create a stand-alone mule sub-class. But that kind of defeats the purpose of OOP. Creating a new sub-class when I already have the traits is a violation of the DRY principle.

To confirm that this is an appropriate way to create my mule I asked myself two questions:

1) Is a mule a horse?

2) Is a mule a donkey?

The answer seemed to be a resounding kind of that leans towards a yes.

I'm completely lost as to how this would be accomplished with classical inheritance. I could not come up with what I considered a "good" solution with interfaces or abstract classes.

In a language which use prototypical inheritance like JavaScript, I might "selectively breed" a mule by pulling down only the methods and instance variables that applied to a mule. However, this seems to be rather close to creating a brand-new sub-class.

What is the "correct" way to handle this problem in both classical and prototypical inheritance?

回答1:

The concept you are looking for is traits (you actually mentioned it). I will use a different example, that I find more appropriate:

trait Engine {
    public function startEngine() {
        echo 'Vrooom';
    }
}

trait Saddle {
    public function rideOnSaddle() {
        echo 'I feel the wind';
    }
}

interface Vehicle {
    public function go();
}

class Car extends Vehicle {
    use Engine;

    public function go() {
        echo $this->startEngine();
    }
}

class Bike extends Vehicle {
    use Saddle;

    public function go() {
        echo $this->rideOnSaddle();
    }
}

class Motorcycle extends Vehicle {
    use Engine;
    use Saddle;

    public function go() {
        echo $this->startEngine();
        echo $this->rideOnSaddle(); 
    }
}

Further reading: Traits in PHP, Traits in Javascript.



回答2:

At first the answer seemed obvious; create a stand-alone mule sub-class. But that kind of defeats the purpose of OOP. Creating a new sub-class when I already have the traits is a violation of the DRY principle.

Decomposition might help reaching the DRY goal.

Every behavior/role that not obviously should be inherited, might be considered of being implemented as mixin or trait. Thus theirs code reuse at different places at class level is much easier and more elegant now via composition.

As for JavaScript, there is only delegation. Inheritance at one hand is supported by an implicit delegation automatism via the prototype chain, whereas composition gets achieved by delegating functionality explicitly via call or apply.

This makes things much easier since one only needs to deal with objects/instances and methods/function-objects. The class/inheritance part in JavaScript is covered by either constructor functions and every constructor function's prototype or by (blueprint-)objects that are passed as prototypes to Object.create. Factories will be useful in providing an API and hiding the preferred implementation of one of the above mentioned approaches.

Nevertheless the core principles remain untouched with both of them ... dealing with a) objects and function-objects with b) inheritance and with c) composition.

The following provided example therefore chooses just ECMAScript-3 features and constructor functions. From there it can be easily transferred(/transpiled) to either class syntax or Object.create.

The OP's example is well chosen, for a mule is neither a horse nor a donkey. It still does belong to the genus Equus but features its very own chromosome pairs that are distinct from the one of horses or donkeys. Yet it features behavior and visible markers of both of them. Inheritance therefore, if at all, is achieved via the Equus. Other behavior and appearance that is either specific or generic to each of both species just will be mixed into a mule.

Function based mixins / traits / talents in JavaScript always get applied to objects/instances, thus even behavior that will be inherited via the prototype chain, can be collected into such functionality that again can be applied to a prototypal object if necessary/appropriate.

The following example makes use of this technique and also does comment on it in order to demonstrate DRY-ness and code reuse at this 2 delegation levels of JavaScript.

var
  INITIAL_STATES_CONFIG = {
    equus: {
      specifics: {
          type: "equus"
      }/*,
      generics: {
      }*/
    },
    horse: {
      specifics: {
          type: "horse"
      }/*,
      generics: {
      }*/
    },
    donkey: {
      specifics: {
          type: "donkey"
      }/*,
      generics: {
      }*/
    },
    mule: {
      specifics: {
          type: "mule"
      }/*,
      generics: {
      }*/
    }
  };



function withToeltGait() { // function based mixin/trait/talent.
  this.toelt = function () {
    return "... tölt ...";
  };
  return this;
}


function withEquusGenerics(/* state */) { // function based mixin/trait/talent composite.
  var
    equus = this;

  // implementation of equus generics.

  equus.walk = function () {
    return "... walk ...";
  };
  equus.trot = function () {
    return "... trot ...";
  };
  equus.gallop = function () {
    return "... gallop ...";
  };
  withToeltGait.call(equus); // composition: use/apply specific equus trait.

  return equus;
}
function withEquusSpecifics(state ) { // function based mixin/trait/talent.
  var
    equus = this;

  // implementation of equus specifics.

  equus.valueOf = function () {
    return Object.assign({}, state);
  };
  equus.toString = function () {
    return JSON.stringify(state);
  };

  return equus;
}

function Equus(state) { // constructor, kept generic via mixin/trait/talent composition.
  state = ((typeof state === 'object') && state) || {};
  var
    equus = this;

  withEquusSpecifics.call(equus, state); // composition: use/apply specific equus trait.

  return equus;
}
// equus inheritance via trait based generic equus composite object.
Equus.prototype = withEquusGenerics.call(new Equus/*, state */);


console.log("Equus.prototype.valueOf() : ", Equus.prototype.valueOf());
console.log("Equus.prototype.toString() : ", Equus.prototype.toString());

console.log("Equus.prototype.walk() : ", Equus.prototype.walk());
console.log("Equus.prototype.trot() : ", Equus.prototype.trot());
console.log("Equus.prototype.toelt() : ", Equus.prototype.toelt());
console.log("Equus.prototype.gallop() : ", Equus.prototype.gallop());

console.log("\n");


var equus = new Equus(INITIAL_STATES_CONFIG.equus.specifics);

console.log("equus.valueOf() : ", equus.valueOf());
console.log("equus.toString() : ", equus.toString());

console.log("equus instanceof Equus ? ", (equus instanceof Equus));


console.log("+++ +++ +++\n\n");



function withHorseGenerics(/* state */) { // function based mixin/trait/talent.
  /*
    implementation of horse generics.
  */
  var
    horse = this;

  // almost all of today's horse breeds lost theirs genetic tölt predisposition.
  horse.toelt = function () {};

  horse.alwaysAlertedAndFleeQuickly = function () {
    return "... always alerted and flee quickly ...";
  };
  return horse;
}
function withHorseSpecifics(/* state */) { // function based mixin/trait/talent.
  /*
    implementation of horse specifics.
  */
  return this;
}

function Horse(state) { // constructor, kept generic via mixin/trait/talent composition.
  state = ((typeof state === 'object') && state) || {};
  var
    horse = this;

  Equus.call(horse, state);                   // - fulfilling proper equus composition.
  withHorseSpecifics.call(horse/*, state */); // - composition: use/apply specific horse trait.

  return horse;
}
// equus inheritance together with generic horse trait composition.
Horse.prototype = withHorseGenerics.call(new Equus/*, state */);


var horse = new Horse(INITIAL_STATES_CONFIG.horse.specifics);

console.log("horse.valueOf() : ", horse.valueOf());
console.log("horse.toString() : ", horse.toString());

console.log("horse instanceof Horse ? ", (horse instanceof Horse));
console.log("horse instanceof Equus ? ", (horse instanceof Equus));

console.log("horse.walk() : ", horse.walk());
console.log("horse.trot() : ", horse.trot());
console.log("horse.toelt() : ", horse.toelt());
console.log("horse.gallop() : ", horse.gallop());

console.log("horse.alwaysAlertedAndFleeQuickly() : ",
  (horse.alwaysAlertedAndFleeQuickly && horse.alwaysAlertedAndFleeQuickly())
);
console.log("horse.beAttentiveCalculateAndRatherFight() : ",
  (horse.beAttentiveCalculateAndRatherFight && horse.beAttentiveCalculateAndRatherFight())
);
console.log("\n");


var toeltingHorse = new Horse(INITIAL_STATES_CONFIG.horse.specifics);
withToeltGait.call(toeltingHorse);

console.log("toeltingHorse.valueOf() : ", toeltingHorse.valueOf());
console.log("toeltingHorse instanceof Horse ? ", (toeltingHorse instanceof Horse));
console.log("toeltingHorse instanceof Equus ? ", (toeltingHorse instanceof Equus));
console.log("toeltingHorse.toelt() : ", toeltingHorse.toelt());

console.log("+++ +++ +++\n\n");



function withDonkeyGenerics(/* state */) { // function based mixin/trait/talent.
  /*
    implementation of donkey generics.
  */
  var
    donkey = this;

  // donkey breeds, as far as I know, still have the genetic
  // predisposition for tölt, but they need to get trained.
  //
  // donkey.toelt = function () {};

  donkey.beAttentiveCalculateAndRatherFight = function () {
    return "... be attentive, calculate and rather fight ...";
  };
  return donkey;
}
function withDonkeySpecifics(/* state */) { // function based mixin/trait/talent.
  /*
    implementation of donkey specifics.
  */
  return this;
}

function Donkey(state) { // constructor, kept generic via mixin/trait/talent composition.
  state = ((typeof state === 'object') && state) || {};
  var
    donkey = this;

  Equus.call(donkey, state);                    // - fulfilling proper equus composition.
  withDonkeySpecifics.call(donkey/*, state */); // - composition: use/apply specific donkey trait.

  return donkey;
}
// equus inheritance together with generic donkey trait composition.
Donkey.prototype = withDonkeyGenerics.call(new Equus/*, state */);


var donkey = new Donkey(INITIAL_STATES_CONFIG.donkey.specifics);

console.log("donkey.valueOf() : ", donkey.valueOf());
console.log("donkey.toString() : ", donkey.toString());

console.log("donkey instanceof Donkey ? ", (donkey instanceof Donkey));
console.log("donkey instanceof Equus ? ", (donkey instanceof Equus));

console.log("donkey.walk() : ", donkey.walk());
console.log("donkey.trot() : ", donkey.trot());
console.log("donkey.toelt() : ", donkey.toelt());
console.log("donkey.gallop() : ", donkey.gallop());

console.log("donkey.alwaysAlertedAndFleeQuickly() : ",
  (donkey.alwaysAlertedAndFleeQuickly && donkey.alwaysAlertedAndFleeQuickly())
);
console.log("donkey.beAttentiveCalculateAndRatherFight() : ",
  (donkey.beAttentiveCalculateAndRatherFight && donkey.beAttentiveCalculateAndRatherFight())
);
console.log("+++ +++ +++\n\n");



function withMuleGenerics(/* state */) { // function based mixin/trait/talent composite.
  /*
    implementation of mule generics.
  */
  var
    mule = this;

  withDonkeyGenerics.call(mule/*, state */);  // composition: use/apply generic donkey trait.
  /*
    add or delete mule generic properties afterwards.
  */

  withHorseGenerics.call(mule/*, state */);   // composition: use/apply generic horse trait.
  /*
    add or delete mule generic properties afterwards.
  */

  // a mules genetic predisposition for tölt is inherited by its mother horse.
  // therefore via calling `withHorseGenerics` this trait gets disabled too by default.

  // when facing danger a mule behaves like a donkey; it rather will fight than flee.
  mule.alwaysAlertedAndFleeQuickly = function () {};

  return mule;
}
function withMuleSpecifics(/* state */) { // function based mixin/trait/talent composite.
  /*
    implementation of mule specifics.
  */
  var
    mule = this;

  withDonkeySpecifics.call(mule/*, state */); // composition: use/apply specific donkey trait.
  /*
    add or delete mule specific properties afterwards.
  */

  withHorseSpecifics.call(mule/*, state */);  // composition: use/apply specific horse trait.
  /*
    add or delete mule specifics properties afterwards.
  */

  return mule;
}

function Mule(state) { // constructor, kept generic via mixin/trait/talent composition.
  state = ((typeof state === 'object') && state) || {};
  var
    mule = this;

  Equus.call(mule, state);                  // - fulfilling proper equus composition.
  withMuleSpecifics.call(mule/*, state */); // - composition: use/apply specific mule trait.

  return mule;
}
// equus inheritance together with generic mule trait composition.
Mule.prototype = withMuleGenerics.call(new Equus/*, state */);


var mule = new Mule(INITIAL_STATES_CONFIG.mule.specifics);

console.log("mule.valueOf() : ", mule.valueOf());
console.log("mule.toString() : ", mule.toString());

console.log("mule instanceof Mule ? ", (mule instanceof Mule));
console.log("mule instanceof Equus ? ", (mule instanceof Equus));

console.log("mule instanceof Donkey ? ", (mule instanceof Donkey));
console.log("mule instanceof Horse ? ", (mule instanceof Horse));

console.log("mule.walk() : ", mule.walk());
console.log("mule.trot() : ", mule.trot());
console.log("mule.toelt() : ", mule.toelt());
console.log("mule.gallop() : ", mule.gallop());

console.log("mule.alwaysAlertedAndFleeQuickly() : ",
  (mule.alwaysAlertedAndFleeQuickly && mule.alwaysAlertedAndFleeQuickly())
);
console.log("mule.beAttentiveCalculateAndRatherFight() : ",
  (mule.beAttentiveCalculateAndRatherFight && mule.beAttentiveCalculateAndRatherFight())
);
console.log("\n");


var toeltingMule = new Mule(INITIAL_STATES_CONFIG.mule.specifics);
withToeltGait.call(toeltingMule);

console.log("toeltingMule.valueOf() : ", toeltingMule.valueOf());
console.log("toeltingMule instanceof Mule ? ", (toeltingMule instanceof Mule));
console.log("toeltingMule instanceof Equus ? ", (toeltingMule instanceof Equus));
console.log("toeltingMule.toelt() : ", toeltingMule.toelt());

console.log("+++ +++ +++\n\n");

side note - recommended resources on functions based Mixins / Traits / Talents in JavaScript

  • A fresh look at JavaScript Mixins by Angus Croll from May 2011
  • The many talents of JavaScript for generalizing Role Oriented Programming approaches like Traits and Mixins from April 2014.

Additionally I do recommend reading some of the listed answers of mine given on SO, that are related to this topic too.

  • Traits in javascript
  • How to use mixins properly in Javascript
  • ES 6 Classes - Mixins