Композиция, наследование и агрегация в JavaScript

есть много информации о композиции против наследования в интернете, но я не нашел достойных примеров с JavaScript. Используя приведенный ниже код для демонстрации наследования:

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(); // 5.19

(чтобы сделать код меньше, вы можете опустить реализации метода, например:Stock.total = function(){ /* code */ } Я просто положил их туда, чтобы быть фантазии). Если композиция предпочтительна для многих ситуаций в ООП, почему большинство людей, использующих JavaScript, используют только прототипы и наследование? Я не нашел много информации о композиции в JavaScript online, только на других языках.

может кто-нибудь дать мне пример, используя приведенный выше код, чтобы продемонстрировать композиция и агрегация?

3 ответов


язык не имеет значения при работе с композицией против наследования. Если вы понимаете, что такое класс и что такое экземпляр класса, то у вас есть все, что вам нужно.

композиция просто когда класс в составе других классов; или, говоря другими словами, экземпляр объекта имеет ссылки на экземпляры других объектов.

наследование, когда класс наследует методы и свойства от другого класс.

предположим, у вас есть две функции, A и B. Вы хотите определить третью функциональность, C, которая имеет некоторые или все из A и B. Вы можете либо сделать c расширяться от B и A, в этом случае C имеет все B и A, потому что C isA B и A, или вы можете сделать каждый экземпляр C иметь экземпляр A и экземпляр B и вызывать элементы в этих функциях. В последнем случае каждый экземпляр C фактически обертывает экземпляр B и экземпляр А.

конечно, в зависимости от языка вы не сможете расширить класс из 2 классов (например, Java не поддерживает множественное наследование), но это специфическая деталь языка, которая не имеет ничего общего с концепцией.

теперь, для конкретных деталей языка...

я использовал слово класс, но javascript не имеет понятия о классе как таковом. У него есть объекты, и это все (кроме простых типов). Использование Javascript прототипное наследование, что означает, что у него есть способ эффективного определения объектов и методов на этих объектах (это тема для другого вопроса; Вы можете искать так, как уже есть ответы.)

Итак, идя с нашим примером выше, у вас есть A, B и C.

для наследования, вы бы

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

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

если бы вы хотели, чтобы C расширил A, вы бы сделали

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

теперь каждый экземпляр C будет иметь метод someMethod, потому что каждый экземпляр C " isA " A.

Javascript не имеет множественного наследования* (подробнее об этом позже), поэтому вы не можете расширить C как A, так и B. Вы можете использовать композицию, однако, чтобы дать ей функциональность. Действительно, это одна из причин, по которой композиция предпочтительнее наследования; нет никаких ограничений на объединение функций (но это не единственная причина).

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

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

Итак, есть ваши простые примеры как для наследования, так и для композиции. Однако, это еще не конец истории. Я уже говорил, что Javascript не поддерживает множественное наследование, и в некотором смысле это не так, потому что вы не можете основывать прототип объекта на прототипах нескольких объектов; т. е. вы не можете сделать

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

потому что как только вы сделаете третью строку, вы просто отмените вторую строку. Это имеет последствия для instanceof оператора.

однако это не имеет значения, потому что просто потому, что вы не можете переопределить конструктор объекта дважды, вы все еще можете добавить любые методы, которые вы хотите прототип объекта. Так что просто потому что вы не можете сделать выше примере вы все еще можете добавить все, что хотите C. prototype, включая все методы на прототипах как A, так и B.

многие фреймворки поддерживают это и упрощают. Я делаю много работы Sproutcore; с этой структурой вы можете сделать

A = {
   method1: function(){}
}

B = {
   method2: function(){}
}

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

здесь я определил функцию в объект литералы A и B, а затем добавил функции как C, поэтому каждый экземпляр C имеет методы 1, 2 и 3. В этом конкретном случае extend метод (предусмотренный рамками) делает весь тяжелый подъем установки прототипов объектов.

EDIT -- в ваших комментариях вы задаете хороший вопрос, а именно: "если вы используете композицию, как вы согласовываете область основного объекта с областью объектов, из которых основной объект состоит".

есть куча способов. Первый-просто передать аргументы. Так что

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

здесь вы не возитесь с областями, вы просто передаете аргументы. Это простой и очень жизнеспособный подход. (аргументы могут исходить из this или быть переданным, что угодно...)

другой способ сделать это - использовать call (или apply) методы javascript, которые в основном позволяют установить область функция.

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

чтобы быть немного более ясным, следующее эквивалентно

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

в javascript функции являются объектами, поэтому вы можете назначить их переменным.

the call вызов здесь устанавливает область someMethodOnA to this, который является экземпляром С.


... Может кто-нибудь дать мне пример, используя приведенный выше код, чтобы продемонстрировать состав и агрегация?

на первый взгляд приведенный пример не кажется лучшим выбор для демонстрации композиции в JavaScript. The prototype свойство Stock функция конструктора по-прежнему остается идеальной место для обоих методов total и list для обоих получите доступ к любому запасу собственные свойства объекта.

что можно сделать развязка реализаций этих методов из прототипа конструкторов и предоставления их обратно точно там - еще в дополнительной форме повторного использования кода-миксины ...

пример:

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

существует много информации о композиции против наследования онлайн, но я не нашел достойных примеров с JavaScript. ...

Я не нашел много информации о композиции в JavaScript онлайн, только на других языках. ...

возможно, поисковый запрос был недостаточно конкретным, но даже в 2012 году поиск "JavaScript Mixin composition" должен был привести к не такое уж плохое направление.

... Если композиция предпочтительна для многих ситуаций в ООП, как большинство людей, использующих JavaScript, похоже, используют только прототипы а наследство?

потому что большинство из них используют то, что они получили, и / или то, что они знакомы. Может быть, так и должно быть. больше распространение знаний о JavaScript как языке на основе делегирования тоже и что может достигните с ним.

приложение:

это связанные темы, недавно обновленный и, надеюсь, помогает ...


Я думаю, что могу показать вам, как переписать ваш код в моде "композиция объектов", используя простой JavaScript (ES5). Я использую заводские функции вместо функции конструктора для создания экземпляра объекта, поэтому нет new ключевое слово нужно. Таким образом, я могу оказать услугу увеличение объекта (композиция) над классическим/псевдоклассическим/прототипным наследованием, так что нет Object.create вызывается функция.

полученный объект хороший плоский объект:

/*
 * 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(); // 5.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);
  };
};

Скрипка здесь.