2012-01-02 41 views
57

Hay mucha información sobre composición vs herencia en línea, pero no he encontrado ejemplos decentes con JavaScript. Con el siguiente código para demostrar la herencia:Composición, herencia y agregación en 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 

(para hacer el código más pequeño, se puede dejar de lado las implementaciones de métodos, tales como: Stock.total = function(){ /* code */ } I Sólo hay que poner ahí a ser de lujo). Si la composición se ve favorecida en muchas situaciones en OOP, ¿cómo es posible que la mayoría de las personas que usan JavaScript solo utilicen prototipos y herencia? No encontré mucha información sobre composición en JavaScript en línea, solo en otros idiomas.

¿Alguien me puede dar un ejemplo usando el código anterior para demostrar la composición y agregación?

+1

La pregunta es demasiado vaga. Probablemente una mejor opción para [programmers.se] (http://programmers.stackexchange.com). –

+1

Si es demasiado vago, IMO no es adecuado para ningún sitio. –

+3

¿Cómo podría ser más específico? Presento código usando herencia, ahora estoy interesado en verlo construido usando composición. Hay otras preguntas que son iguales para otros idiomas. – Brian

Respuesta

72

El lenguaje es irrelevante cuando se trata de composición vs herencia. Si comprende qué clase es y qué instancia es de una clase, entonces tiene todo lo que necesita.

La composición es simplemente cuando una clase es compuesta por de otras clases; o para decirlo de otra manera, una instancia de un objeto tiene referencias a instancias de otros objetos.

Herencia es cuando una clase hereda métodos y propiedades de otra clase.

Digamos que tiene dos funciones, A y B. Desea definir una tercera funcionalidad, C, que tiene algunas o todas de A y B. Puede hacer que C se extienda desde B y A, en cuyo caso C tiene todo lo que B y A tienen porque C isA B y A, o puede hacer que cada instancia de C tenga una instancia de A y una instancia de B, e invoque elementos en esas funcionalidades. En el último caso, cada instancia C en efecto envuelve una instancia de B y una instancia de A.

Por supuesto, dependiendo del idioma, es posible que no pueda tener una clase de 2 clases (por ejemplo, Java no es compatible con herencia múltiple), pero ese es un detalle específico del idioma que no tiene nada que ver con el concepto.

Ahora, para los detalles específicos del lenguaje ...

he utilizado la palabra clase, pero JavaScript no tiene noción de clase como tal. Tiene objetos, y eso es (aparte de los tipos simples). Javascript usa herencia prototípica, lo que significa que tiene una forma de definir eficientemente los objetos y los métodos en esos objetos (este es el tema para otra pregunta, puede buscar SO ya que hay respuestas)

Así que vamos con nuestro ejemplo anteriormente, que tiene a, B, y C.

Por herencia, que tendría

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

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

Si quería C para extender a, que haría

Ahora cada instancia de C tendría el método someMethod, porque cada instancia de C "ISA" A.

Javascript no tiene herencia múltiple * (más sobre esto más adelante), por lo que no se puede tener C se extiende A y B. Sin embargo, puede usar la composición para darle la funcionalidad.De hecho, esta es una de las razones por las que la composición es preferida por algunos sobre la herencia; no hay límites para combinar la funcionalidad (pero esta no es la única razón).

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

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

Así que hay ejemplos sencillos para la herencia y la composición. Sin embargo, este no es el final de la historia. Dije antes que el Javascript no es compatible con la herencia múltiple, y en cierto sentido no, porque no se puede basar el prototipo de un objeto en los prototipos de objetos múltiples; es decir, no se puede hacer

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

porque tan pronto como lo hace la tercera, línea, acaba de deshacer la segunda línea. Esto tiene implicaciones para el operador instanceof.

Sin embargo, esto realmente no importa, porque simplemente porque no se puede redefinir el constructor de un objeto dos veces, todavía puede añadir cualquier método que desea el prototipo de un objeto. Así que sólo porque no se puede hacer el ejemplo anterior, todavía se puede añadir todo lo que quiere C.prototype, incluyendo todos los métodos en los prototipos de ambos

Muchos marcos A y B.

apoyar esto y hacerlo fácil. Hago mucho trabajo de Sproutcore; con dicho marco que puede hacer

A = { 
    method1: function(){} 
} 

B = { 
    method2: function(){} 
} 

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

Aquí he definido funcionalidad en objetos literales A y B, y después se añadió la funcionalidad de ambos para C, por lo que cada instancia de C tiene los métodos 1, 2 y 3. En este caso particular, el método extend (proporcionado por el marco) hace todo el trabajo pesado de configurar los prototipos de los objetos.

EDITAR - En sus comentarios, saca una buena pregunta, es decir, "Si utiliza la composición, ¿cómo concilia el alcance del objeto principal con el alcance de los objetos de los que se compone el objeto principal".

Hay muchas maneras. El primero es simplemente pasar argumentos. Así que

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

Aquí no estás jugando con telescopios, simplemente estás pasando argumentos. Este es un enfoque simple y muy viable. (Los argumentos pueden venir de this o ser aprobada en, lo que sea ...)

Otra manera de hacerlo sería utilizar los call (o apply) métodos de Javascript, que básicamente le permite establecer el alcance de una función.

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

a ser un poco más claro, lo siguiente es equivalente

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

en JavaScript, las funciones son objeto, por lo que puede asignarlos a variables.

la call invocación aquí es establecer el alcance de someMethodOnA a this, que es la instancia de C.

+0

'this.b.someMethod' en su ejemplo no sabe nada sobre' this'. ¿Cómo calculará el total o enumerará las existencias? – katspaugh

+0

katspaugh, correcto - Lo corregí. Golpeando todo esto sin realmente ejecutarlo ... – hvgotcodes

+0

katspaugh, 'someMethod' en b no sabe acerca de' esto' en A, eso es cierto. Sin embargo, puede escribir el método para aceptar argumentos generales. Si quería avanzar realmente, podría usar la opción llamar y postularse; Voy a actualizar mi respuesta – hvgotcodes

3

... Puede alguien dar un ejemplo usando el código anterior para demostrar composición y agregación ?

A primera vista, el ejemplo proporcionado no parece ser la mejor opción para demostrar la composición en JavaScript. El prototype propiedad de la función Stock constructor sigue siendo el lugar ideal para ambos métodos total y list tanto para no tener acceso a las propiedades propias de cualquier del objeto.

Lo que se puede hacer es desacoplar las implementaciones de estos métodos desde el prototipo constructores y la disponibilidad de nuevo exactamente allí - sin embargo, en una forma adicional de la reutilización de código - Mixins ...

ejemplo:

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

Hay una gran cantidad de información acerca de la composición en comparación con la herencia en línea, pero no he encontrado ejemplos correctos con JavaScript. ...

No encontré mucha información acerca de la composición en JavaScript en línea, solo en otros idiomas. ...

Tal vez la consulta de búsqueda no era lo suficientemente específica pero incluso en 2012 la búsqueda de "composición de JavaScript Mixin" debería haber conducido a un no es tan malo dirección.

... Si la composición se ve favorecida por una gran cantidad de situaciones en la programación orientada a objetos, cómo llegado la mayoría de las personas que utilizan JavaScript parecen sólo prototipos de uso y la herencia?

Porque la mayoría de ellos usan, lo que obtuvieron y/o lo que ellos están familiarizados. Tal vez debería haber más conocimiento difundido sobre JavaScript como idioma basado en la delegación también y qué se puede lograr con él.

apéndice: hilos

Esta están relacionados, recientemente actualizados y espero ayudar ...

2

I th tinta Puedo mostrarle cómo reescribir su código en la forma de "composición de objetos" utilizando JavaScript plano (ES5). Uso funciones de fábrica en lugar de las funciones de constructor para crear una instancia de objeto, por lo que no es necesaria la palabra clave new. De esta forma, puedo favorecer aumento de objetos (composición) sobre herencia clásica/pseudo-clásica/prototípica, por lo que no se llama a la función Object.create.

El objeto resultante es un objeto-compuesta plana agradable:

/* 
* 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 here.

Cuestiones relacionadas