web-dev-qa-db-fra.com

Composition, héritage et agrégation en JavaScript

Il y a beaucoup d'informations sur la composition vs l'héritage en ligne, mais je n'ai pas trouvé d'exemples décents avec JavaScript. Utilisation du code ci-dessous pour démontrer l'héritage:

function Stock( /* object with stock names and prices */ ) {
    for (var company_name in arguments[0]) {
        // copy the passed object into the new object created by the constructor
        this[company_name] = arguments[0][company_name]; 
    }
}

// example methods in prototype, their implementation is probably redundant for
// this question, but list() returns an array with toString() invoked; total()
// adds up the stock prices and returns them. Using ES5 feature to make
// inherited properties non-enumerable 

Stock.prototype =  {
    list: function () {
        var company_list = [];
        for (var company_name in this)
            company_list.Push(company_name);
        return company_list.toString();
    },
    total: function () {
        var price_total = 0;
        for (var company_name in this)
            price_total += this[company_name];
        return '$' + price_total;
    }
};

Object.defineProperties(Stock.prototype, {
    list: { enumerable: false },
    total: { enumerable:false }
});

var portfolio = new Stock({ MSFT: 25.96, YHOO: 16.13, AMZN: 173.10 });
portfolio.list();  // MSFT,YHOO,AMZN
portfolio.total(); // $215.19

(Pour rendre le code plus petit, vous pouvez laisser de côté les implémentations de la méthode, comme: Stock.total = function(){ /* code */ } Je viens de les mettre dedans pour être fantaisiste). Si la composition est privilégiée pour de nombreuses situations dans la POO, comment se fait-il que la plupart des gens utilisant JavaScript semblent n'utiliser que des prototypes et l'héritage? Je n'ai pas trouvé beaucoup d'informations sur la composition en JavaScript en ligne, uniquement dans d'autres langues.

Quelqu'un peut-il me donner un exemple en utilisant le code ci-dessus pour démontrer la composition et l'agrégation?

57
Brian

Le langage n'est pas pertinent lorsqu'il s'agit de composition vs héritage. Si vous comprenez ce qu'est une classe et ce qu'est une instance d'une classe, alors vous avez tout ce dont vous avez besoin.

La composition est simplement quand une classe est composée d'autres classes; ou pour le dire autrement, une instance d'un objet a des références à des instances d'autres objets.

L'héritage est lorsqu'une classe hérite des méthodes et des propriétés d'une autre classe.

Disons que vous avez deux fonctionnalités, A et B. Vous voulez définir une troisième fonctionnalité, C, qui a tout ou partie des deux A et B. Vous pouvez soit faire étendre C à partir de B et A, auquel cas C a tout B et A a parce que C isA B et A, ou vous pouvez faire en sorte que chaque instance de C ait une instance de A et une instance de B, et invoquer des éléments sur ces fonctionnalités. Dans ce dernier cas, chaque instance C enveloppe en fait une instance de B et une instance de A.

Bien sûr, selon la langue, vous ne pourrez peut-être pas étendre une classe à partir de 2 classes (par exemple Java ne prend pas en charge l'héritage multiple), mais c'est un détail spécifique à la langue qui n'a rien à voir avec le concept.

Maintenant, pour les détails spécifiques à la langue ...

J'ai utilisé Word class, mais javascript n'a aucune notion de classe en tant que telle. Il a des objets, et c'est tout (autre que les types simples). Javascript utilise l'héritage prototypique, ce qui signifie qu'il a un moyen de définir efficacement les objets et les méthodes sur ces objets (c'est le sujet d'une autre question; vous pouvez rechercher SO car il y a déjà des réponses.)

Donc, en suivant notre exemple ci-dessus, vous avez A, B et C.

Pour l'héritage, vous auriez

// define an object (which can be viewed as a "class")
function A(){}

// define some functionality
A.prototype.someMethod = function(){}

Si vous vouliez que C étende A, vous feriez

C.prototype = new A();
C.prototype.constructor = A;

Maintenant, chaque instance de C aurait la méthode someMethod, car chaque instance de C "est A" A.

Javascript n'a pas d'héritage multiple * (plus à ce sujet plus tard), vous ne pouvez donc pas avoir C étendre A et B. Vous pouvez cependant utiliser la composition pour lui donner la fonctionnalité. En effet, c'est l'une des raisons pour lesquelles la composition est préférée par certains à l'héritage; il n'y a pas de limites à la combinaison de fonctionnalités (mais ce n'est pas la seule raison).

function C(){
   this.a = new A();
   this.b = new B();
}

// someMethod on C invokes the someMethod on B.
C.someMethod = function(){
    this.a.someMethod()
}

Il y a donc vos exemples simples d'héritage et de composition. Cependant, ce n'est pas la fin de l'histoire. J'ai dit auparavant que Javascript ne prend pas en charge l'héritage multiple, et dans un sens ce n'est pas le cas, car vous ne pouvez pas baser le prototype d'un objet sur les prototypes de plusieurs objets; c'est-à-dire que vous ne pouvez pas faire

C.prototype = new B();
C.prototype.constructor = B;
C.prototype.constructor = A;

parce que dès que vous faites la troisième ligne, vous venez de défaire la deuxième ligne. Cela a des implications pour l'opérateur instanceof.

Cependant, cela n'a pas vraiment d'importance, car simplement parce que vous ne pouvez pas redéfinir le constructeur d'un objet deux fois, vous pouvez toujours ajouter les méthodes de votre choix au prototype d'un objet. Donc, juste parce que vous ne pouvez pas faire l'exemple ci-dessus, vous pouvez toujours ajouter tout ce que vous voulez au C.prototype, y compris toutes les méthodes sur les prototypes de A et B.

De nombreux cadres prennent en charge cela et facilitent la tâche. Je fais beaucoup de travail Sproutcore; avec ce cadre, vous pouvez faire

A = {
   method1: function(){}
}

B = {
   method2: function(){}
}

C = SC.Object.extend(A, B, {
   method3: function(){}
}

Ici, j'ai défini la fonctionnalité dans les littéraux d'objet A et B, puis j'ai ajouté la fonctionnalité des deux à C, donc chaque instance de C a les méthodes 1, 2 et 3. Dans ce cas particulier, la méthode extend (fournie par le framework) fait tout le gros du travail de mise en place des prototypes des objets.

EDIT - Dans vos commentaires, vous soulevez une bonne question, à savoir "Si vous utilisez la composition, comment conciliez-vous la portée de l'objet principal avec la portée des objets qui composent l'objet principal".

Il y a plusieurs façons. La première consiste simplement à passer des arguments. Donc

C.someMethod = function(){
    this.a.someMethod(arg1, arg2...);
}

Ici, vous ne jouez pas avec les portées, vous passez simplement des arguments. Il s'agit d'une approche simple et très viable. (les arguments peuvent provenir de this ou être passés, peu importe ...)

Une autre façon de le faire serait d'utiliser les méthodes call (ou apply) de javascript, qui vous permettent essentiellement de définir la portée d'une fonction.

C.someMethod = function(){
    this.a.someMethod.call(this, arg1, arg2...);
}

pour être un peu plus clair, ce qui suit est équivalent

C.someMethod = function(){
    var someMethodOnA = this.a.someMethod;
    someMethodOnA.call(this, arg1, arg2...);
}

En javascript, les fonctions sont des objets, vous pouvez donc les affecter à des variables.

l'invocation call ici définit la portée de someMethodOnA sur this, qui est l'instance de C.

75
hvgotcodes

... Quelqu'un peut-il me donner un exemple en utilisant le code ci-dessus pour démontrer la composition et l'agrégation?

À première vue, l'exemple fourni ne semble pas être le meilleur choix pour démontrer la composition en JavaScript. La propriété prototype de la fonction constructeur Stock reste toujours l'endroit idéal pour les deux méthodes total et list car toutes deux accèdent aux propriétés de tout objet stock.

Ce qui peut être fait est de découpler les implémentations de ces méthodes du prototype des constructeurs et de les fournir exactement là-bas - mais sous une forme supplémentaire de réutilisation de code - Mixins ...

exemple:

var Iterable_listAllKeys = (function () {

    var
        Mixin,

        object_keys = Object.keys,

        listAllKeys = function () {
            return object_keys(this).join(", ");
        }
    ;

    Mixin = function () {
        this.list = listAllKeys;
    };

    return Mixin;

}());


var Iterable_computeTotal = (function (global) {

  var
      Mixin,

      currencyFlag,

      object_keys = global.Object.keys,
      parse_float = global.parseFloat,

      aggregateNumberValue = function (collector, key) {
          collector.value = (
              collector.value
              + parse_float(collector.target[key], 10)
          );
          return collector;
      },
      computeTotal = function () {
          return [

              currencyFlag,
              object_keys(this)
                  .reduce(aggregateNumberValue, {value: 0, target: this})
                  .value
                  .toFixed(2)

          ].join(" ");
      }
    ;

    Mixin = function (config) {
        currencyFlag = (config && config.currencyFlag) || "";

        this.total = computeTotal;
    };

    return Mixin;

}(this));


var Stock = (function () {

  var
      Stock,

      object_keys = Object.keys,

      createKeyValueForTarget = function (collector, key) {
          collector.target[key] = collector.config[key];
          return collector;
      },
      createStock = function (config) { // Factory
          return (new Stock(config));
      },
      isStock = function (type) {
          return (type instanceof Stock);
      }
  ;

  Stock = function (config) { // Constructor
      var stock = this;
      object_keys(config).reduce(createKeyValueForTarget, {

          config: config,
          target: stock
      });
      return stock;
  };

  /**
   *  composition:
   *  - apply both mixins to the constructor's prototype
   *  - by delegating them explicitly via [call].
   */
  Iterable_listAllKeys.call(Stock.prototype);
  Iterable_computeTotal.call(Stock.prototype, {currencyFlag: "$"});

  /**
   *  [[Stock]] factory module
   */
  return {
      isStock : isStock,
      create  : createStock
  };

}());


var stock = Stock.create({MSFT: 25.96, YHOO: 16.13, AMZN: 173.10});

/**
 *  both methods are available due to JavaScript's
 *  - prototypal delegation automatism that covers inheritance.
 */
console.log(stock.list());
console.log(stock.total());

console.log(stock);
console.dir(stock);

Il y a beaucoup d'informations sur la composition vs l'héritage en ligne, mais je n'ai pas trouvé d'exemples décents avec JavaScript. ...

Je n'ai pas trouvé beaucoup d'informations sur la composition en JavaScript en ligne, uniquement dans d'autres langues. ...

Peut-être que la requête de recherche n'était pas suffisamment spécifique, mais même en 2012, la recherche de "composition JavaScript Mixin" aurait dû conduire dans une direction pas si mauvaise.

... Si la composition est privilégiée pour de nombreuses situations dans la POO, comment se fait-il que la plupart des gens utilisant JavaScript semblent n'utiliser que des prototypes et l'héritage?

Parce que la plupart d'entre eux utilisent ce qu'ils ont appris et/ou ce qu'ils connaissent. Peut-être devrait-il y avoir plus de connaissances sur JavaScript en tant que langage basé sur la délégation et sur ce qui peut être réalisé avec.

appendice:

Ce sont des fils connexes, récemment mis à jour et qui, espérons-le, aideront ...

2
Peter Seliger

Je pense que je peux vous montrer comment réécrire votre code de manière "composition d'objet" en utilisant du JavaScript simple (ES5). J'utilise les fonctions d'usine au lieu des fonctions de constructeur pour créer une instance d'objet, donc pas de mot clé new nécessaire. De cette façon, je peux favoriser l'augmentation d'objet (composition) par rapport à l'héritage classique/pseudo-classique/prototypique, donc non Object.create la fonction est appelée.

L'objet résultant est un bel objet composé à plat:

/*
 * Factory function for creating "abstract stock" object. 
 */
var AbstractStock = function (options) {

  /**
   * Private properties :)
   * @see http://javascript.crockford.com/private.html
   */
  var companyList = [],
      priceTotal = 0;

  for (var companyName in options) {

    if (options.hasOwnProperty(companyName)) {
      companyList.Push(companyName);
      priceTotal = priceTotal + options[companyName];
    }
  }

  return {
    /**
     * Privileged methods; methods that use private properties by using closure. ;)
     * @see http://javascript.crockford.com/private.html
     */
    getCompanyList: function () {
      return companyList;
    },
    getPriceTotal: function () {
      return priceTotal;
    },
    /*
     * Abstract methods
     */
    list: function () {
      throw new Error('list() method not implemented.');
    },
    total: function () {
      throw new Error('total() method not implemented.');
    }
  };
};

/*
 * Factory function for creating "stock" object.
 * Here, since the stock object is composed from abstract stock
 * object, you can make use of properties/methods exposed by the 
 * abstract stock object.
 */
var Stock = compose(AbstractStock, function (options) {

  return {
    /*
     * More concrete methods
     */
    list: function () {
      console.log(this.getCompanyList().toString());
    },
    total: function () {
      console.log('$' + this.getPriceTotal());
    }
  };
});

// Create an instance of stock object. No `new`! (!)
var portofolio = Stock({MSFT: 25.96, YHOO: 16.13, AMZN: 173.10});
portofolio.list(); // MSFT,YHOO,AMZN
portofolio.total(); // $215.19

/*
 * No deep level of prototypal (or whatsoever) inheritance hierarchy;
 * just a flat object inherited directly from the `Object` prototype.
 * "What could be more object-oriented than that?" –Douglas Crockford
 */ 
console.log(portofolio); 



/*
 * Here is the magic potion:
 * Create a composed factory function for creating a composed object.
 * Factory that creates more abstract object should come first. 
 */
function compose(factory0, factoryN) {
  var factories = arguments;

  /*
   * Note that the `options` passed earlier to the composed factory
   * will be passed to each factory when creating object.
   */
  return function (options) {

    // Collect objects after creating them from each factory.
    var objects = [].map.call(factories, function(factory) {
      return factory(options);
    });

    // ...and then, compose the objects.
    return Object.assign.apply(this, objects);
  };
};

Violon ici .

2
Glenn Mohammad