programacion - Composición, herencia y agregación en JavaScript
programacion de objetos en javascript (3)
Hay mucha información sobre composición vs herencia en línea, pero no he encontrado ejemplos decentes con JavaScript. Usando el siguiente código para demostrar la herencia:
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 que el código sea más pequeño, puede omitir las implementaciones del método, como: Stock.total = function(){ /* code */ }
Simplemente las puse allí para ser elegante). 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.
¿Puede alguien darme un ejemplo usando el código anterior para demostrar la composición y agregación?
... ¿Alguien puede darme un ejemplo usando el código anterior para demostrar la composición y la agregación?
A primera vista, el ejemplo proporcionado no parece ser la mejor opción para demostrar la composición en JavaScript. La propiedad prototype
de la función constructor de Stock
sigue siendo el lugar ideal para ambos métodos, total
y list
para ambos acceden a las propiedades de cualquier objeto de stock.
Lo que se puede hacer es desacoplar las implementaciones de estos métodos del prototipo de constructores y proporcionarlos exactamente allí, pero en una forma adicional de 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 mucha información sobre composición vs herencia en línea, pero no he encontrado ejemplos decentes con JavaScript. ...
No encontré mucha información sobre composición en JavaScript en línea, solo en otros idiomas. ...
Tal vez la consulta de búsqueda no fue lo suficientemente específica, pero incluso en 2012 buscar "Composición de JavaScript Mixin" debería haber llevado a una dirección no tan mala.
... 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?
Debido a que la mayoría de ellos usan, lo que obtuvieron dijo y / o con lo que están familiarizados. Tal vez debería haber más conocimiento extendido sobre JavaScript como un lenguaje basado en la delegación y qué se puede lograr con él.
apéndice:
Estos son hilos relacionados, actualizados recientemente y con suerte ayudando ...
- .com :: Cómo usar mixins correctamente en Javascript
- .com :: Rasgos en javascript
- .com :: Javascript Distingue entre Composición vs. Herencia
Creo que puedo mostrarte cómo reescribir tu código en la forma de "composición de objetos" utilizando JavaScript plano (ES5). Uso funciones de fábrica en lugar de funciones de constructor para crear una instancia de objeto, por lo que no es necesaria una new
palabra clave. De esta forma, puedo favorecer el aumento de objetos (composición) sobre la herencia clásica / Object.create
/ prototípica , por lo que no se Object.create
ninguna función Object.create
.
El objeto resultante es un bonito objeto compuesto de forma plana:
/*
* 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);
};
};
Violín here .
El lenguaje es irrelevante cuando se trata de composición vs herencia. Si entiendes qué clase es y qué instancia de una clase es, entonces tienes todo lo que necesitas.
La composición es simplemente cuando una clase se compone de otras clases; o para decirlo de otra manera, una instancia de un objeto tiene referencias a instancias de otros objetos.
La herencia es cuando una clase hereda métodos y propiedades de otra clase.
Supongamos que tiene dos funciones, A y B. Desea definir una tercera funcionalidad, C, que tiene algunas o todas, tanto A como B. Puede hacer que C se extienda desde B y A, en cuyo caso C tiene todo B y A tiene porque C es 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 admite 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 idioma ...
Usé la palabra clase , pero javascript no tiene ninguna noción de clase como tal. Tiene objetos, y eso es (aparte de los tipos simples). Javascript usa la 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 yendo con nuestro ejemplo anterior, tienes A, B y C.
Por herencia, tendrías
// define an object (which can be viewed as a "class")
function A(){}
// define some functionality
A.prototype.someMethod = function(){}
Si quisieras que C extienda A, lo harías
C.prototype = new A();
C.prototype.constructor = 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 puede hacer que C extienda tanto A como 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()
}
Entonces, hay ejemplos simples 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 puedes hacer
C.prototype = new B();
C.prototype.constructor = B;
C.prototype.constructor = A;
porque tan pronto como hagas la tercera línea, simplemente deshaces la segunda línea. Esto tiene implicaciones para el operador de instanceof
.
Sin embargo, esto realmente no importa, porque simplemente porque no puede redefinir el constructor de un objeto dos veces, aún puede agregar los métodos que desee al prototipo de un objeto . Entonces, simplemente porque no puede hacer el ejemplo anterior, puede agregar todo lo que desee a C.prototype , incluidos todos los métodos en los prototipos de A y B.
Muchos marcos soportan esto y lo hacen fácil. Hago mucho trabajo de Sproutcore; con ese marco que puedes hacer
A = {
method1: function(){}
}
B = {
method2: function(){}
}
C = SC.Object.extend(A, B, {
method3: function(){}
}
Aquí definí la funcionalidad en los literales de objeto A
y B
, y luego agregué la funcionalidad de ambos a C
, por lo que cada instancia de C tiene los métodos 1, 2 y 3. En este caso particular, el método de extend
(provisto por el marco) no todo el trabajo pesado de configurar los prototipos de los objetos.
EDITAR - En sus comentarios, saca una buena pregunta, es decir, "Si usa 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. Asi 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 provenir de this
o ser pasados, lo que sea ...)
Otra forma de hacerlo sería usar los métodos call
(o apply
) de javascript, que básicamente le permite establecer el alcance de una función.
C.someMethod = function(){
this.a.someMethod.call(this, arg1, arg2...);
}
para 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 asignarlas a variables.
la invocación de call
aquí está estableciendo el alcance de someMethodOnA
a this
, que es la instancia de C.