Ho abbastanza la funzione di consentire alle classi di essere definite con ereditarietà multipla. Permette codice come il seguente. Complessivamente noterai una completa deviazione dalle tecniche di classificazione native in javascript (ad esempio non vedrai mai la class
parola chiave):
let human = new Running({ name: 'human', numLegs: 2 });
human.run();
let airplane = new Flying({ name: 'airplane', numWings: 2 });
airplane.fly();
let dragon = new RunningFlying({ name: 'dragon', numLegs: 4, numWings: 6 });
dragon.takeFlight();
per produrre output in questo modo:
human runs with 2 legs.
airplane flies away with 2 wings!
dragon runs with 4 legs.
dragon flies away with 6 wings!
Ecco come appaiono le definizioni delle classi:
let Named = makeClass('Named', {}, () => ({
init: function({ name }) {
this.name = name;
}
}));
let Running = makeClass('Running', { Named }, protos => ({
init: function({ name, numLegs }) {
protos.Named.init.call(this, { name });
this.numLegs = numLegs;
},
run: function() {
console.log(`${this.name} runs with ${this.numLegs} legs.`);
}
}));
let Flying = makeClass('Flying', { Named }, protos => ({
init: function({ name, numWings }) {
protos.Named.init.call(this, { name });
this.numWings = numWings;
},
fly: function( ){
console.log(`${this.name} flies away with ${this.numWings} wings!`);
}
}));
let RunningFlying = makeClass('RunningFlying', { Running, Flying }, protos => ({
init: function({ name, numLegs, numWings }) {
protos.Running.init.call(this, { name, numLegs });
protos.Flying.init.call(this, { name, numWings });
},
takeFlight: function() {
this.run();
this.fly();
}
}));
Possiamo vedere che ogni definizione di classe usando la makeClass
funzione accetta un nome Object
di classe genitore associato a classi genitore. Accetta anche una funzione che restituisce una Object
proprietà contenente per la classe che viene definita. Questa funzione ha un parametro protos
, che contiene informazioni sufficienti per accedere a qualsiasi proprietà definita da una delle classi principali.
L'ultimo pezzo richiesto è la makeClass
funzione stessa, che fa un bel po 'di lavoro. Eccolo, insieme al resto del codice. Ho commentato makeClass
abbastanza pesantemente:
let makeClass = (name, parents={}, propertiesFn=()=>({})) => {
// The constructor just curries to a Function named "init"
let Class = function(...args) { this.init(...args); };
// This allows instances to be named properly in the terminal
Object.defineProperty(Class, 'name', { value: name });
// Tracking parents of `Class` allows for inheritance queries later
Class.parents = parents;
// Initialize prototype
Class.prototype = Object.create(null);
// Collect all parent-class prototypes. `Object.getOwnPropertyNames`
// will get us the best results. Finally, we'll be able to reference
// a property like "usefulMethod" of Class "ParentClass3" with:
// `parProtos.ParentClass3.usefulMethod`
let parProtos = {};
for (let parName in parents) {
let proto = parents[parName].prototype;
parProtos[parName] = {};
for (let k of Object.getOwnPropertyNames(proto)) {
parProtos[parName][k] = proto[k];
}
}
// Resolve `properties` as the result of calling `propertiesFn`. Pass
// `parProtos`, so a child-class can access parent-class methods, and
// pass `Class` so methods of the child-class have a reference to it
let properties = propertiesFn(parProtos, Class);
properties.constructor = Class; // Ensure "constructor" prop exists
// If two parent-classes define a property under the same name, we
// have a "collision". In cases of collisions, the child-class *must*
// define a method (and within that method it can decide how to call
// the parent-class methods of the same name). For every named
// property of every parent-class, we'll track a `Set` containing all
// the methods that fall under that name. Any `Set` of size greater
// than one indicates a collision.
let propsByName = {}; // Will map property names to `Set`s
for (let parName in parProtos) {
for (let propName in parProtos[parName]) {
// Now track the property `parProtos[parName][propName]` under the
// label of `propName`
if (!propsByName.hasOwnProperty(propName))
propsByName[propName] = new Set();
propsByName[propName].add(parProtos[parName][propName]);
}
}
// For all methods defined by the child-class, create or replace the
// entry in `propsByName` with a Set containing a single item; the
// child-class' property at that property name (this also guarantees
// there is no collision at this property name). Note property names
// prefixed with "$" will be considered class properties (and the "$"
// will be removed).
for (let propName in properties) {
if (propName[0] === '$') {
// The "$" indicates a class property; attach to `Class`:
Class[propName.slice(1)] = properties[propName];
} else {
// No "$" indicates an instance property; attach to `propsByName`:
propsByName[propName] = new Set([ properties[propName] ]);
}
}
// Ensure that "init" is defined by a parent-class or by the child:
if (!propsByName.hasOwnProperty('init'))
throw Error(`Class "${name}" is missing an "init" method`);
// For each property name in `propsByName`, ensure that there is no
// collision at that property name, and if there isn't, attach it to
// the prototype! `Object.defineProperty` can ensure that prototype
// properties won't appear during iteration with `in` keyword:
for (let propName in propsByName) {
let propsAtName = propsByName[propName];
if (propsAtName.size > 1)
throw new Error(`Class "${name}" has conflict at "${propName}"`);
Object.defineProperty(Class.prototype, propName, {
enumerable: false,
writable: true,
value: propsAtName.values().next().value // Get 1st item in Set
});
}
return Class;
};
let Named = makeClass('Named', {}, () => ({
init: function({ name }) {
this.name = name;
}
}));
let Running = makeClass('Running', { Named }, protos => ({
init: function({ name, numLegs }) {
protos.Named.init.call(this, { name });
this.numLegs = numLegs;
},
run: function() {
console.log(`${this.name} runs with ${this.numLegs} legs.`);
}
}));
let Flying = makeClass('Flying', { Named }, protos => ({
init: function({ name, numWings }) {
protos.Named.init.call(this, { name });
this.numWings = numWings;
},
fly: function( ){
console.log(`${this.name} flies away with ${this.numWings} wings!`);
}
}));
let RunningFlying = makeClass('RunningFlying', { Running, Flying }, protos => ({
init: function({ name, numLegs, numWings }) {
protos.Running.init.call(this, { name, numLegs });
protos.Flying.init.call(this, { name, numWings });
},
takeFlight: function() {
this.run();
this.fly();
}
}));
let human = new Running({ name: 'human', numLegs: 2 });
human.run();
let airplane = new Flying({ name: 'airplane', numWings: 2 });
airplane.fly();
let dragon = new RunningFlying({ name: 'dragon', numLegs: 4, numWings: 6 });
dragon.takeFlight();
La makeClass
funzione supporta anche le proprietà di classe; questi sono definiti prefissando i nomi delle proprietà con il $
simbolo (si noti che il nome della proprietà finale risultante verrà $
rimosso). Con questo in mente, potremmo scrivere una Dragon
classe specializzata che modella il "tipo" del Drago, dove l'elenco dei tipi di Drago disponibili è memorizzato nella Classe stessa, al contrario delle istanze:
let Dragon = makeClass('Dragon', { RunningFlying }, protos => ({
$types: {
wyvern: 'wyvern',
drake: 'drake',
hydra: 'hydra'
},
init: function({ name, numLegs, numWings, type }) {
protos.RunningFlying.init.call(this, { name, numLegs, numWings });
this.type = type;
},
description: function() {
return `A ${this.type}-type dragon with ${this.numLegs} legs and ${this.numWings} wings`;
}
}));
let dragon1 = new Dragon({ name: 'dragon1', numLegs: 2, numWings: 4, type: Dragon.types.drake });
let dragon2 = new Dragon({ name: 'dragon2', numLegs: 4, numWings: 2, type: Dragon.types.hydra });
Le sfide dell'ereditarietà multipla
Chiunque abbia seguito da makeClass
vicino il codice noterà un fenomeno indesiderabile piuttosto significativo che si verifica silenziosamente quando viene eseguito il codice sopra: la creazione di un'istanza RunningFlying
comporterà DUE chiamate al Named
costruttore!
Questo perché il grafico dell'ereditarietà è simile al seguente:
(^^ More Specialized ^^)
RunningFlying
/ \
/ \
Running Flying
\ /
\ /
Named
(vv More Abstract vv)
Quando ci sono più percorsi per la stessa classe genitore in un grafico dell'ereditarietà della sottoclasse, le istanze della sottoclasse richiameranno più volte il costruttore di quella classe genitore.
Combattere questo non è banale. Diamo un'occhiata ad alcuni esempi con nomi di classe semplificati. Considereremo la classe A
, la classe genitore più astratta, le classi B
e C
, entrambe ereditate da A
, e la classe BC
che eredita da B
e C
(e quindi concettualmente "doppia eredità" da A
):
let A = makeClass('A', {}, () => ({
init: function() {
console.log('Construct A');
}
}));
let B = makeClass('B', { A }, protos => ({
init: function() {
protos.A.init.call(this);
console.log('Construct B');
}
}));
let C = makeClass('C', { A }, protos => ({
init: function() {
protos.A.init.call(this);
console.log('Construct C');
}
}));
let BC = makeClass('BC', { B, C }, protos => ({
init: function() {
// Overall "Construct A" is logged twice:
protos.B.init.call(this); // -> console.log('Construct A'); console.log('Construct B');
protos.C.init.call(this); // -> console.log('Construct A'); console.log('Construct C');
console.log('Construct BC');
}
}));
Se vogliamo impedire il BC
doppio A.prototype.init
richiamo, potremmo aver bisogno di abbandonare lo stile di chiamare direttamente i costruttori ereditati. Avremo bisogno di un certo livello di riferimento indiretto per verificare se si verificano chiamate duplicate e cortocircuitare prima che si verifichino.
Potremmo prendere in considerazione la modifica dei parametri forniti alla funzione proprietà: a fianco protos
, Object
contenente un dato non elaborato che descrive le proprietà ereditate, potremmo anche includere una funzione di utilità per chiamare un metodo di istanza in modo tale che vengano chiamati anche metodi parent, ma vengono rilevate chiamate duplicate e prevenuto. Diamo un'occhiata a dove stabiliamo i parametri per propertiesFn
Function
:
let makeClass = (name, parents, propertiesFn) => {
/* ... a bunch of makeClass logic ... */
// Allows referencing inherited functions; e.g. `parProtos.ParentClass3.usefulMethod`
let parProtos = {};
/* ... collect all parent methods in `parProtos` ... */
// Utility functions for calling inherited methods:
let util = {};
util.invokeNoDuplicates = (instance, fnName, args, dups=new Set()) => {
// Invoke every parent method of name `fnName` first...
for (let parName of parProtos) {
if (parProtos[parName].hasOwnProperty(fnName)) {
// Our parent named `parName` defines the function named `fnName`
let fn = parProtos[parName][fnName];
// Check if this function has already been encountered.
// This solves our duplicate-invocation problem!!
if (dups.has(fn)) continue;
dups.add(fn);
// This is the first time this Function has been encountered.
// Call it on `instance`, with the desired args. Make sure we
// include `dups`, so that if the parent method invokes further
// inherited methods we don't lose track of what functions have
// have already been called.
fn.call(instance, ...args, dups);
}
}
};
// Now we can call `propertiesFn` with an additional `util` param:
// Resolve `properties` as the result of calling `propertiesFn`:
let properties = propertiesFn(parProtos, util, Class);
/* ... a bunch more makeClass logic ... */
};
L'intero scopo della modifica di cui sopra makeClass
è in modo che abbiamo un argomento aggiuntivo fornito al nostro propertiesFn
quando invochiamo makeClass
. Dobbiamo anche essere consapevoli del fatto che ogni funzione definita in qualsiasi classe ora può ricevere un parametro dopo tutti gli altri, denominati dup
, che è uno Set
che contiene tutte le funzioni che sono già state chiamate come risultato della chiamata al metodo ereditato:
let A = makeClass('A', {}, () => ({
init: function() {
console.log('Construct A');
}
}));
let B = makeClass('B', { A }, (protos, util) => ({
init: function(dups) {
util.invokeNoDuplicates(this, 'init', [ /* no args */ ], dups);
console.log('Construct B');
}
}));
let C = makeClass('C', { A }, (protos, util) => ({
init: function(dups) {
util.invokeNoDuplicates(this, 'init', [ /* no args */ ], dups);
console.log('Construct C');
}
}));
let BC = makeClass('BC', { B, C }, (protos, util) => ({
init: function(dups) {
util.invokeNoDuplicates(this, 'init', [ /* no args */ ], dups);
console.log('Construct BC');
}
}));
Questo nuovo stile riesce effettivamente a garantire che "Construct A"
venga registrato solo una volta quando BC
viene inizializzata un'istanza di . Ma ci sono tre aspetti negativi, il terzo dei quali è molto critico :
- Questo codice è diventato meno leggibile e gestibile. Molta complessità si nasconde dietro la
util.invokeNoDuplicates
funzione e pensare a come questo stile eviti la multi-invocazione non è intuitivo e induce mal di testa. Abbiamo anche quel dups
parametro fastidioso , che deve davvero essere definito su ogni singola funzione della classe . Ahia.
- Questo codice è più lento - è necessario un po 'più di indiretta e di calcolo per ottenere risultati desiderabili con eredità multipla. Sfortunatamente, questo è probabilmente il caso di qualsiasi soluzione al nostro problema di invocazione multipla.
- Più significativamente, la struttura delle funzioni che si basano sull'eredità è diventata molto rigida . Se una sottoclasse
NiftyClass
sovrascrive una funzione niftyFunction
e la utilizza util.invokeNoDuplicates(this, 'niftyFunction', ...)
per eseguirla senza invocazione duplicata, NiftyClass.prototype.niftyFunction
chiamerà la funzione denominata niftyFunction
di ogni classe genitrice che la definisce, ignora qualsiasi valore restituito da tali classi e infine esegue la logica specializzata di NiftyClass.prototype.niftyFunction
. Questa è l' unica struttura possibile . Se NiftyClass
eredita CoolClass
e GoodClass
, ed entrambe queste classi principali forniscono niftyFunction
definizioni proprie, NiftyClass.prototype.niftyFunction
non potranno mai (senza rischiare l'invocazione multipla):
- A. Esegui prima la logica specializzata
NiftyClass
, quindi la logica specializzata delle classi genitore
- B. Esegui la logica specializzata
NiftyClass
in qualsiasi momento diverso dal completamento di tutta la logica padre specializzata
- C. Comportarsi condizionatamente in base ai valori di ritorno della logica specializzata dei suoi genitori
- D. Evitare l'esecuzione di un genitore particolare è specializzata
niftyFunction
del tutto
Ovviamente, potremmo risolvere ogni problema segnalato sopra definendo funzioni specializzate sotto util
:
- A. definire
util.invokeNoDuplicatesSubClassLogicFirst(instance, fnName, ...)
- B. define
util.invokeNoDuplicatesSubClassAfterParent(parentName, instance, fnName, ...)
(Dov'è parentName
il nome del genitore la cui logica specializzata sarà immediatamente seguita dalla logica specializzata delle classi figlio)
- C. define
util.invokeNoDuplicatesCanShortCircuitOnParent(parentName, testFn, instance, fnName, ...)
(In questo caso testFn
riceverebbe il risultato della logica specializzata per il genitore nominato parentName
e restituirebbe un true/false
valore che indica se il cortocircuito dovrebbe avvenire)
- D. define
util.invokeNoDuplicatesBlackListedParents(blackList, instance, fnName, ...)
(In questo caso blackList
sarebbe uno Array
dei nomi parent la cui logica specializzata dovrebbe essere saltata del tutto)
Queste soluzioni sono tutte disponibili, ma questo è un caos totale ! Per ogni struttura unica che può assumere una chiamata di funzione ereditata, avremmo bisogno di un metodo specializzato definito in util
. Che disastro assoluto.
Con questo in mente possiamo iniziare a vedere le sfide legate all'implementazione di una buona eredità multipla. La piena implementazione di makeClass
I fornita in questa risposta non considera nemmeno il problema di invocazione multipla o molti altri problemi che sorgono riguardo l'ereditarietà multipla.
Questa risposta sta diventando molto lunga. Spero che l' makeClass
implementazione che ho incluso sia ancora utile, anche se non è perfetta. Spero anche che chiunque sia interessato a questo argomento abbia guadagnato più contesto da tenere a mente mentre leggono ulteriormente!