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 class
sł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 makeClass
funkcji akceptuje Object
nazwy klas rodzicielskich odwzorowane na klasy nadrzędne. Akceptuje również funkcję, która zwraca Object
wł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 makeClass
sama funkcja, która wykonuje całkiem sporo pracy. Oto ona, wraz z resztą kodu. Skomentowałem makeClass
dość 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();
makeClass
Funkcja 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ą Dragon
klasę, 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 makeClass
uważ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 RunningFlying
spowoduje DWIE wywołania Named
konstruktora!
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 B
i C
, które zarówno dziedziczą po A
, jak i klasę, BC
która dziedziczy z B
i 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ąć BC
podwó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
, Object
zawierają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 makeClass
jest to, że mamy dodatkowy argument dostarczony do naszego, propertiesFn
gdy 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, Set
któ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 BC
jest 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.invokeNoDuplicates
funkcją 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 dups
parametr, 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
NiftyClass
przesłania funkcjęniftyFunction
i użyje jejutil.invokeNoDuplicates(this, 'niftyFunction', ...)
do uruchomienia bez wywołania duplikatu,NiftyClass.prototype.niftyFunction
wywoła funkcję o nazwieniftyFunction
każ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śliNiftyClass
dziedziczyCoolClass
iGoodClass
, a obie te klasy nadrzędne dostarczająniftyFunction
własnych definicji,NiftyClass.prototype.niftyFunction
nigdy (bez ryzyka wielokrotnego wywołania) nie będą w stanie:
- A. Uruchom
NiftyClass
najpierw wyspecjalizowaną logikę , a następnie wyspecjalizowaną logikę klas rodzicielskich
- B. Uruchom wyspecjalizowaną logikę
NiftyClass
w 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
niftyFunction
ogó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 parentName
jest 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 testFn
otrzyma wynik wyspecjalizowanej logiki dla nazwanego rodzica parentName
i zwróci true/false
wartość wskazującą, czy powinno nastąpić zwarcie)
- RE. zdefiniować
util.invokeNoDuplicatesBlackListedParents(blackList, instance, fnName, ...)
(w tym przypadku blackList
byłoby to Array
nazwami 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, makeClass
któ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!