Композиция, наследование и агрегация в 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 онлайн, только на других языках.

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

3 57

3 ответа:

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

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

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

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

конечно, в зависимости от языка вы не можете иметь класс extend от 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.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 до this, который является экземпляром С.

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

на первый взгляд приведенный пример не кажется лучшим выбор для того, чтобы продемонстрировать композицию в JavaScript. Элемент 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, похоже, используют только прототипы а наследство?

потому что большинство из них используют, что они получили tought и / или что они знакомы с ним. Может быть, там должно быть распространение новых знаний о 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);
  };
};

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