Подтвердить что ты не робот

Состав, наследование и агрегация в 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(); // $215.19

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

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

4b9b3361

Ответ 1

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

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

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

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

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

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

Я использовал класс слова, но у javascript нет понятия класса как такового. У него есть объекты, и это он (кроме простых типов). 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 или передаваться, независимо от того...)

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

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

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

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

Приложение:

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

Ответ 3

Я думаю, что я могу показать вам, как переписать свой код в стиле объектов, используя простой JavaScript (ES5). Я использую factory функции вместо функций конструктора для создания экземпляра объекта, поэтому не требуется ключевое слово 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);
  };
};

Fiddle здесь.