Mam całkiem funkcję umożliwiającą definiowanie klas z wielokrotnym dziedziczeniem. Pozwala na kod podobny do następującego. Ogólnie zauważysz całkowite odejście od natywnych technik klasyfikowania w javascript (np. Nigdy nie zobaczysz classsłowa kluczowego):
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();
aby wygenerować taki wynik:
human runs with 2 legs.
airplane flies away with 2 wings!
dragon runs with 4 legs.
dragon flies away with 6 wings!
Oto jak wyglądają definicje klas:
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();
}
}));
Widzimy, że każda definicja klasy używająca makeClassfunkcji akceptuje Objectnazwy klas rodzicielskich odwzorowane na klasy nadrzędne. Akceptuje również funkcję, która zwraca Objectwłaściwości zawierające dla definiowanej klasy. Ta funkcja ma parametr protos, który zawiera wystarczającą ilość informacji, aby uzyskać dostęp do dowolnej właściwości zdefiniowanej przez którąkolwiek z klas nadrzędnych.
Ostatnim wymaganym elementem jest makeClasssama funkcja, która wykonuje całkiem sporo pracy. Oto ona, wraz z resztą kodu. Skomentowałem makeClassdość mocno:
let makeClass = (name, parents={}, propertiesFn=()=>({})) => {
let Class = function(...args) { this.init(...args); };
Object.defineProperty(Class, 'name', { value: name });
Class.parents = parents;
Class.prototype = Object.create(null);
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];
}
}
let properties = propertiesFn(parProtos, Class);
properties.constructor = Class;
let propsByName = {};
for (let parName in parProtos) {
for (let propName in parProtos[parName]) {
if (!propsByName.hasOwnProperty(propName))
propsByName[propName] = new Set();
propsByName[propName].add(parProtos[parName][propName]);
}
}
for (let propName in properties) {
if (propName[0] === '$') {
Class[propName.slice(1)] = properties[propName];
} else {
propsByName[propName] = new Set([ properties[propName] ]);
}
}
if (!propsByName.hasOwnProperty('init'))
throw Error(`Class "${name}" is missing an "init" method`);
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
});
}
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();
makeClassFunkcja obsługuje także właściwości klasy; są one definiowane przez poprzedzanie nazw właściwości $symbolem (zwróć uwagę, że ostateczna nazwa właściwości, która zostanie wywołana, zostanie $usunięta). Mając to na uwadze, moglibyśmy napisać wyspecjalizowaną Dragonklasę, która modeluje „typ” Smoka, gdzie lista dostępnych typów Smoka jest przechowywana w samej klasie, w przeciwieństwie do instancji:
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 });
Wyzwania wielokrotnego dziedziczenia
Każdy, kto makeClassuważnie śledził kod , zauważy dość znaczące niepożądane zjawisko występujące po cichu, gdy powyższy kod zostanie uruchomiony: utworzenie wystąpienia a RunningFlyingspowoduje DWIE wywołania Namedkonstruktora!
Dzieje się tak, ponieważ wykres dziedziczenia wygląda następująco:
(^^ More Specialized ^^)
RunningFlying
/ \
/ \
Running Flying
\ /
\ /
Named
(vv More Abstract vv)
Gdy istnieje wiele ścieżek do tej samej klasy nadrzędnej na wykresie dziedziczenia podklasy, wystąpienia tej podklasy będą wielokrotnie wywoływać konstruktor tej klasy nadrzędnej.
Zwalczanie tego jest nietrywialne. Spójrzmy na kilka przykładów z uproszczonymi nazwami klas. Rozważymy klasę A, najbardziej abstrakcyjną klasę-rodzica, klasy Bi C, które zarówno dziedziczą po A, jak i klasę, BCktóra dziedziczy z Bi C(a zatem koncepcyjnie „dziedziczy podwójne” z 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() {
protos.B.init.call(this);
protos.C.init.call(this);
console.log('Construct BC');
}
}));
Jeśli chcemy uniknąć BCpodwójnego wywoływania A.prototype.init, może zaistnieć potrzeba porzucenia stylu bezpośredniego wywoływania konstruktorów dziedziczonych. Będziemy potrzebować pewnego poziomu pośrednictwa, aby sprawdzić, czy występują zduplikowane połączenia, i zwarcia, zanim się pojawią.
Możemy rozważyć zmianę parametrów dostarczanej do nieruchomości funkcjonować: obok protos, Objectzawierające surowe dane opisujące odziedziczone właściwości, możemy także funkcję użytkową dla wywołanie metody instancji w taki sposób, że metody macierzyste nazywane są również, ale są wykrywane zduplikowane połączenia i zapobiec. Przyjrzyjmy się, gdzie ustalamy parametry propertiesFn Function:
let makeClass = (name, parents, propertiesFn) => {
let parProtos = {};
let util = {};
util.invokeNoDuplicates = (instance, fnName, args, dups=new Set()) => {
for (let parName of parProtos) {
if (parProtos[parName].hasOwnProperty(fnName)) {
let fn = parProtos[parName][fnName];
if (dups.has(fn)) continue;
dups.add(fn);
fn.call(instance, ...args, dups);
}
}
};
let properties = propertiesFn(parProtos, util, Class);
};
Celem powyższej zmiany makeClassjest to, że mamy dodatkowy argument dostarczony do naszego, propertiesFngdy wywołujemy makeClass. Powinniśmy również mieć świadomość, że każda funkcja zdefiniowana w dowolnej klasie może teraz otrzymać parametr po wszystkich swoich nazwach dup, czyli a, Setktóry zawiera wszystkie funkcje, które zostały już wywołane w wyniku wywołania odziedziczonej metody:
let A = makeClass('A', {}, () => ({
init: function() {
console.log('Construct A');
}
}));
let B = makeClass('B', { A }, (protos, util) => ({
init: function(dups) {
util.invokeNoDuplicates(this, 'init', [ ], dups);
console.log('Construct B');
}
}));
let C = makeClass('C', { A }, (protos, util) => ({
init: function(dups) {
util.invokeNoDuplicates(this, 'init', [ ], dups);
console.log('Construct C');
}
}));
let BC = makeClass('BC', { B, C }, (protos, util) => ({
init: function(dups) {
util.invokeNoDuplicates(this, 'init', [ ], dups);
console.log('Construct BC');
}
}));
Ten nowy styl faktycznie zapewnia, że "Construct A"jest rejestrowany tylko raz, gdy instancja programu BCjest inicjowana. Ale są trzy wady, z których trzecia jest bardzo krytyczna :
- Ten kod stał się mniej czytelny i trudny do utrzymania. Za tą
util.invokeNoDuplicatesfunkcją kryje się duża złożoność , a myślenie o tym, jak ten styl unika wielu wywołań, jest nieintuicyjne i wywołuje ból głowy. Mamy również ten nieznośny dupsparametr, który naprawdę musi być zdefiniowany w każdej funkcji w klasie . Auć.
- Ten kod jest wolniejszy - do uzyskania pożądanych wyników z wielokrotnym dziedziczeniem wymagane jest trochę więcej pośrednich i obliczeniowych. Niestety, prawdopodobnie tak będzie w przypadku każdego rozwiązania naszego problemu z wielokrotnymi wywołaniami.
- Co najważniejsze, stała się struktura funkcji, które opierają się na dziedziczeniu bardzo sztywna . Jeśli podklasa
NiftyClassprzesłania funkcjęniftyFunctioni użyje jejutil.invokeNoDuplicates(this, 'niftyFunction', ...)do uruchomienia bez wywołania duplikatu,NiftyClass.prototype.niftyFunctionwywoła funkcję o nazwieniftyFunctionkażdej klasy nadrzędnej, która ją definiuje, zignoruje wszelkie wartości zwracane z tych klas i na koniec wykona wyspecjalizowaną logikęNiftyClass.prototype.niftyFunction. To jedyna możliwa konstrukcja . JeśliNiftyClassdziedziczyCoolClassiGoodClass, a obie te klasy nadrzędne dostarczająniftyFunctionwłasnych definicji,NiftyClass.prototype.niftyFunctionnigdy (bez ryzyka wielokrotnego wywołania) nie będą w stanie:
- A. Uruchom
NiftyClassnajpierw wyspecjalizowaną logikę , a następnie wyspecjalizowaną logikę klas rodzicielskich
- B. Uruchom wyspecjalizowaną logikę
NiftyClassw dowolnym momencie innym niż po zakończeniu całej wyspecjalizowanej logiki nadrzędnej
- C. Zachowywać się warunkowo w zależności od wartości zwracanych przez wyspecjalizowaną logikę rodzica
- D. Unikaj w
niftyFunctionogóle prowadzenia specjalizacji konkretnego rodzica
Oczywiście, możemy rozwiązać każdy problem z literami powyżej, definiując wyspecjalizowane funkcje w util:
- A. zdefiniować
util.invokeNoDuplicatesSubClassLogicFirst(instance, fnName, ...)
- B. define
util.invokeNoDuplicatesSubClassAfterParent(parentName, instance, fnName, ...)(gdzie parentNamejest nazwą rodzica, którego wyspecjalizowana logika zostanie bezpośrednio poprzedzona wyspecjalizowaną logiką klas potomnych)
- DO. define
util.invokeNoDuplicatesCanShortCircuitOnParent(parentName, testFn, instance, fnName, ...)(W tym przypadku testFnotrzyma wynik wyspecjalizowanej logiki dla nazwanego rodzica parentNamei zwróci true/falsewartość wskazującą, czy powinno nastąpić zwarcie)
- RE. zdefiniować
util.invokeNoDuplicatesBlackListedParents(blackList, instance, fnName, ...)(w tym przypadku blackListbyłoby to Arraynazwami rodziców, których wyspecjalizowana logika powinna zostać całkowicie pominięta)
Wszystkie te rozwiązania są dostępne, ale to totalny chaos ! Dla każdej unikalnej struktury, którą może przyjąć wywołanie funkcji dziedziczonej, potrzebowalibyśmy wyspecjalizowanej metody zdefiniowanej w util. Co za absolutna katastrofa.
Mając to na uwadze, możemy zacząć dostrzegać wyzwania związane z wdrażaniem dobrego wielokrotnego dziedziczenia. Pełna implementacja, makeClassktórą podałem w tej odpowiedzi, nie uwzględnia nawet problemu wielokrotnych wywołań ani wielu innych problemów, które pojawiają się w związku z wielokrotnym dziedziczeniem.
Ta odpowiedź jest bardzo długa. Mam nadzieję, makeClassże dołączona przeze mnie implementacja jest nadal przydatna, nawet jeśli nie jest idealna. Mam również nadzieję, że każdy zainteresowany tym tematem zyskał więcej kontekstu, o którym powinien pamiętać, czytając dalej!