Композиция, наследование и агрегация в JavaScript - PullRequest
57 голосов
/ 02 января 2012

В Интернете много информации о композиции и наследовании, но я не нашел достойных примеров с 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(); // $215.19

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

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

Ответы [ 3 ]

75 голосов
/ 02 января 2012

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

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

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

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

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

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

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

Итак, в нашем примере выше у вас есть 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 (предоставляется каркасом) выполняет всю тяжелую работу по настройке прототипов объектов.

РЕДАКТИРОВАТЬ - В ваших комментариях вы задаете хороший вопрос, а именно: «Если вы используете композицию, как вы согласовываете область действия основного объекта с областью объектов, из которых состоит основной объект».

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

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 функции являются объектами, поэтому вы можете назначитьих переменным.

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

2 голосов
/ 08 декабря 2015

Я думаю, что смогу показать вам, как переписать ваш код в режиме «состав объекта», используя простой 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(); // $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);
  };
};

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

2 голосов
/ 13 октября 2014

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

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

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

пример:

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 состав" должен был привести к не то плохое направление.

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

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

Приложение:

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

...