ugrás a tartalomhoz

Egy JavaScript keretrendszer születése – öröklődés

inf · 2010. Okt. 23. (Szo), 15.51
Egy JavaScript keretrendszer születése – öröklődés

Ebben a részben az osztályok közötti öröklést valósítjuk meg. Mint azt látni fogjuk, ez nem is annyira egyszerű feladat. A javascriptes öröklődésre több megoldás született. Létezik olyan, ahol másoljuk a prototípusok tulajdonságait, és olyan is, ahol a leszármazott osztály prototípusa az ős osztály egy példánya. Vizsgáljuk meg ez utóbbit kicsit közelebbről.

Prototípusos öröklés

A sorozatban megjelent

Az előző részben az osztályok a Class.prototype tulajdonságainak másolásával kapták meg a rájuk jellemző felületet. Ennek a módszernek az a hátránya, hogy a Class.prototype módosítása nem hat ki a már meglévő példányokra, ezért ha új tulajdonságokat akarunk az összes osztályhoz hozzáadni, akkor az ezért felelős kódot vagy a class.js-be tesszük, vagy közvetlenül az után szúrjuk be.

A másolásos örökléssel szemben a prototípusokra támaszkodónak megvan az az előnye, hogy az ős osztály módosításával a leszármazott osztály is módosul. Ugyanezt a viselkedést elérhetnénk másolásos örökléssel és a megfigyelő (observer) mintával, de miért tennénk, ha a JavaScript megoldja helyettünk? Az alábbi kód tartalmazza az esszenciáját a prototípusos öröklésnek.

var Ancestor=function (){};
Ancestor.prototype={data: {}};

var Descendant=function (){};
Descendant.prototype=new Ancestor();

JavaScriptben a prototípus módosítása kihat a már meglévő példányokra is, ezért ha a leszármazott prototípusa az ős egy példánya, akkor az ős prototípusának módosítása kihat a leszármazott prototípusára, és ezzel együtt a leszármazott példányaira is. Az egyedüli gond az initialize()-zal lesz, hiszen azt minden példány létrehozásakor lefuttatja a konstruktor. Ezt a viselkedést majd valahogyan meg kell akadályoznunk, amikor a leszármazottak prototípusait hozzuk létre.

Most írjunk egy szimpla tesztet erre a fajta öröklésre, hogy tényleg úgy működik-e, mint ahogy mondom.

test("basic inheritance test",function ()
{
	var Ancestor=function (){};
	Ancestor.prototype=
	{
		commonData: {},
		privateData: {}
	};
	
	var Descendant=function (){};
	Descendant.prototype=new Ancestor();
	Descendant.prototype.privateData={};

	var ancestor1=new Ancestor();
	var ancestor2=new Ancestor();
	ok(ancestor1.commonData && ancestor1.privateData,"Data is well readable.");
	equals(ancestor1.privateData,ancestor2.privateData,"Data normally the same in every instances.");
	
	var change={};
	Ancestor.prototype.privateData=change;
	equals(change,ancestor1.privateData,"Changing the data in prototype changes data in instances too.");
	
	ancestor2.privateData={};
	ok(ancestor1.privateData!==ancestor2.privateData,"Changing data in instance does not change data in other instances.")
	
	var descendant=new Descendant();
	equals(this.ancestor1.commonData,this.descendant.commonData,"Common (inherited) data normally equals in ancestor and descendant.");
	ok(ancestor1.privateData!==descendant.privateData,"Private data (not inherited - overwritten) does not equal in ancestor and descendant");
	
	var change={};
	Ancestor.prototype.commonData=change;
	ok(change===ancestor1.commonData && change===descendant.commonData,"Changing common data in ancestor changes common data in descendant, so the change is inherited.");
});

Futtassuk a tesztet, minden szép és jó. :-)

Tervezés

Most fogalmazzuk meg, hogy milyen viselkedést várunk el a keretrendszerben az öröklésnél:

  1. A leszármazott osztálynak örökölnie kell az ős példányváltozóit és metódusait, de ezeket az értékeket a leszármazottban felül lehet írni.
  2. Ha az ős osztály módosul, akkor a változás tovább terjed a leszármazottakra és azok leszármazottaira.
  3. Egy leszármazottnak csak egy ős osztálya, de egy ősnek több leszármazottja lehet.
  4. A leszármazott metódusaiból egyszerűen el lehet érni az ős metódusait.

A első és második pontokat a prototípusos öröklés kielégíti.

A harmadik pont meghatározza a felületet:

var Ancestor=new Class({});
var Descendant=Ancestor.extend({});

Az ős osztály utólagos megadásánál egy csomó olyan probléma előkerül, ami az osztály létrehozásakor történő megadásnál nem jelentkezik. Az utólagos megadásnak egyetlen előnye van az azonnalival szemben: az ős osztálynak nem kell léteznie a leszármazott létrejöttekor. Azt hiszem ez a viselkedés nem annyira fontos, sőt talán zavaró is lehet, szóval nyugodtan feladhatjuk a tisztább kód érdekében.

A negyedik pont a fogas kérdés. Csinálhatnánk azt, hogy a metódusokban az őst a neve alapján érjük el.

var Ancestor=new Class(
{
	initialize: function (/*...*/){/*...*/}
});
var Descendant=Ancestor.extend(
{
	initialize: function (/*...*/)
	{
		Ancestor.prototype.initialize.apply(this,arguments);
		/*...*/
	}
});

Ez egy lehetséges megoldás, én viszont szerettem volna kizárni az osztály neveket a dologból, és a lehető legrövidebb szintaxist alkalmazni. A legtöbb nyelvben valamilyen kulcsszóval érik el az aktuális osztály ősét, én is emellett döntöttem. Nekem a Java super kulcsszava tetszett meg, de sajnos JavaScriptben ez fenntartott szó, ezért úgy döntöttem, hogy az ős osztályt superClass néven fogom elérni, az ős osztály prototípusát pedig superMethods néven, hiszen a gyakorlatban csak a metódusokat szoktuk használni az ős osztályból. Az Object.getClass() mintájára ezek is Object metódusok lesznek, hogy minél egyszerűbben el lehessen érni őket. Természetesen az osztályoknál ugyanígy működik majd a dolog, szóval a Class.prototype-ba is bekerülnek majd ezek a metódusok.

Az aktuális osztály ősének elérése nem is annyira egyszerű feladat. Az ember alapból úgy gondolja, hogy az aktuális osztály az adott példány osztálya, pedig ez nem így van:

var A=new Class(
{
	initialize: function ()
	{
		alert("A");
	}
});
var B=A.extend(
{
	initialize: function ()
	{
		this.superMethods().initialize.call(this);
		alert("B");
	}
});
var C=B.extend(
{
	initialize: function ()
	{
		this.superMethods().initialize.call(this);
		alert("C");
	}
});

var c=new C(); //A,B,C

Ennél a kódnál a C.initialize() és a B.initialize() metódusokban eltér a this.superMethods értéke. A B osztály metódusaiban a this.superMethods az A osztályra vonatkozik, a C osztály metódusaiban pedig a B osztályra. Ha a megvalósításnál ezt nem vennénk figyelembe, akkor a fenti kód futtatása végtelen ciklust eredményezne.

Tehát az aktuális osztály valójában az az osztály, ahol az aktuális metódust először megadtuk, és nem az, aminek a példányát éppen használjuk. Úgy gondoltam, hogy jó lesz, ha az aktuális metódust és az aktuális metódus osztályát az ős osztályhoz hasonló módon lehet elérni. Így az aktuális metódusnál az Object.currentMethodre, az aktuális metódus osztályánál pedig az Object.currentClassre esett a választásom. Magában a metódusban is tárolni kell, hogy mi az ő osztálya, úgy döntöttem, hogy ezt pedig a Function.ownerClass-szel lehessen elérni.

A kontextus automatikus beállítása – a this.superMethods().initialize.call(this) helyett a this.superMethods().initialize() formát lehessen használni – túlságosan erőforrás-igényes és komplikált lenne, ezért eltekintünk tőle.

Újrahasznosítható teszt

Az előzőekben megadtuk, hogy milyen felülettel fog dolgozni a rendszer, a továbbiakban a teszt megírására térünk át. Az ember eljátszadozik a gondolattal, hogy mi lenne, ha ugyanezt a tesztet használnánk fel, mint amit már az elején megírtunk, hiszen abban egy csomó dolog újrahasznosítható.

A teszt két fő részből áll: van az osztályok létrehozását, példányosítást és módosítást végző rész, és van az ellenőrző rész. Az ellenőrző rész csak példány tulajdonságokat ér el, ez pedig a keretrendszer használatával is úgy megy, mint alapból. A létrehozó résznél viszont a Class() függvényt és a Class.prototype függvényeit fogjuk használni, így ezt a részt az új tesztben felül kell írni. Mivel a prototípusos öröklés átment a teszten, ezért a későbbiekben felhasználhatjuk magának a teszt kódnak a továbbörökítésére.

Először csináljunk egy BasicInheritanceTest osztályt, a teljes tesztet pedig emeljük be a run() metódusába.

var BasicInheritanceTest=function (){};
BasicInheritanceTest.prototype=
{
	run: function ()
	{
		//...
	}
};

test("basic inheritance test",function ()
{
	new BasicInheritanceTest().run();
});

Utána az összes változóból csináljunk példányváltozót (írjunk elé thist), és emeljük ki külön metódusokba a létrehozó kódot.

BasicInheritanceTest.prototype=
{
	run: function ()
	{
		this.buildAncestorClass();
		this.buildDescendantClass();
		
		this.createAncestorInstances();
		ok(this.ancestor1.commonData && this.ancestor1.privateData,"Data is well readable.");
		equals(this.ancestor1.privateData,this.ancestor2.privateData,"Data normally the same in every instances.");
		
		this.changeAncestorData();
		equals(this.change,this.ancestor1.privateData,"Changing the data in prototype changes data in instances too.");
		
		this.changeAncestorInstanceData();
		ok(this.ancestor1.privateData!==this.ancestor2.privateData,"Changing data in instance does not change data in other instances.")
		
		this.createDescendantInstance();
		equals(this.ancestor1.commonData,this.descendant.commonData,"Common (inherited) data normally equals in ancestor and descendant.");
		ok(this.ancestor1.privateData!==this.descendant.privateData,"Private data (not inherited - overwritten) does not equal in ancestor and descendant");

		this.changeCommonData();
		ok(this.change===this.ancestor1.commonData && this.change===this.descendant.commonData,"Changing common data in ancestor changes common data in descendant, so the change is inherited.");
	},
	buildAncestorClass: function ()
	{
		this.Ancestor=function (){};
		this.Ancestor.prototype=
		{
			commonData: {},
			privateData: {}
		};
	},
	buildDescendantClass: function ()
	{
		this.Descendant=function (){};
		this.Descendant.prototype=new this.Ancestor();
		this.Descendant.prototype.privateData={};
	},
	createAncestorInstances: function ()
	{
		this.ancestor1=new this.Ancestor();
		this.ancestor2=new this.Ancestor();
	},
	changeAncestorData: function ()
	{
		this.change={};
		this.Ancestor.prototype.privateData=this.change;
	},
	changeAncestorInstanceData: function ()
	{
		this.ancestor2.privateData={};
	},
	createDescendantInstance: function ()
	{
		this.descendant=new this.Descendant();
	},
	changeCommonData: function ()
	{
		this.change={};
		this.Ancestor.prototype.commonData=this.change;
	}
};

Kész is, futtassuk a tesztet, még mindig működik.

Most származtassuk az új tesztet a BasicInheritanceTestből:

var InheritanceTest=(function ()
{
	var Test=function (){};
	Test.prototype=new BasicInheritanceTest();
	var proto=
	{
		buildAncestorClass: function ()
		{
			this.Ancestor=new Class(
			{
				commonData: {},
				privateData: {}
			});
		},
		buildDescendantClass: function ()
		{
			this.Descendant=this.Ancestor.extend(
			{
				privateData: {}
			});
		},
		changeAncestorData: function ()
		{
			this.change={};
			this.Ancestor.append(
			{
				privateData: this.change
			});
		},
		changeCommonData: function ()
		{
			this.change={};
			this.Ancestor.append(
			{
				commonData: this.change
			});
		}
	};
	for (var k in proto)
	{
		if (proto.hasOwnProperty(k))
		{
			Test.prototype[k]=proto[k];
		}
	}
	return Test;
})();

test("inheritance test",function ()
{
	new InheritanceTest().run();
});

Futtassuk a tesztet. Nincs szintaktikai hiba, a teszt nem működik, mert hiányolja a Class.extend()-et.

Alap szintű öröklés megvalósítása

Egyelőre most tekintsünk el a teszt kibővítésétől, és haladjunk kisebb lépésekkel. A mostani teszt teljesítéséhez gyakorlatilag annyit kell belevinni a rendszerbe, mint amit az elején bemutattam. Mint már említettem, az initialize()-t blokkolni kell. Ezt egy erre a célra létrehozott változó átadásával érhetjük el a legegyszerűbben. Tegyük is bele ezt a viselkedést a ClassBuilder.newClass()-be, majd futtassuk a tesztet, hogy minden rendben van-e.

var preventDefault={};
var ClassBuilder=
{
	newClass: function ()
	{
		this.current=function ()
		{
			if (this.constructor!==arguments.callee)
			{
				throw new SyntaxError("new class(...) is the right syntax");
			}
			if (
				(this.initialize instanceof Function)
			&&
				(arguments.length!=1 || arguments[0]!==preventDefault)
			)
			{
				this.initialize.apply(this,arguments);
			}
		};
	},
	//...
}

Én mindent rendben találtam, szóval jöhet a Class.extend() megvalósítása.


Class.prototype=
{
	//...
	extend: function (o)
	{
		var Ancestor=this;
		var Descendant=new Class();
		Descendant.prototype=new Ancestor(preventDefault);
		return Descendant.append(o);
	}
};

Elméletileg ennyi lenne, viszont a teszten az új kód megbukik. A következőt kapjuk: Died on test #5: new class(...) is the right syntax. A probléma már ismerős az előző cikkből. Ha a prototípusra saját objektumot állítunk be, akkor annak a constructor tulajdonságát kapják meg a példányok is. Ezért felül kell írni a konstruktor tulajdonságot a prototípusban.

Class.prototype=
{
	//...
	extend: function (o)
	{
		var Ancestor=this;
		var Descendant=new Class();
		Descendant.prototype=new Ancestor(preventDefault);
		Descendant.prototype.constructor=Descendant;
		return Descendant.append(o);
	}
};

Így már átmegy a teszten. Ezzel tehát megvannak az alapok.

Teszt bővítése

Most bővítsük ki az InheritanceTestet a tervezésnél átgondolt Object és Function metódusokra vonatkozó ellenőrzésekkel. Ahhoz, hogy ezek is lefussanak a run()-t is felül kell bírálnunk.


run: function ()
{
	BasicInheritanceTest.prototype.run.call(this);
	
	equals(this.Ancestor,this.Descendant.superClass(),"Reaching class ancestor.");
	
	this.withoutCurrentMethod();
	
	ok(!this.currentMethod,"Reaching empty current method.");
	ok(!this.currentClass,"Reaching empty current class.");
	ok(!this.superClass,"Reaching empty super class.");
	ok(!this.superMethods,"Reaching empty super proto.");
	
	this.createAncestorMethods();

	equals(this.Ancestor,this.Ancestor.prototype.privateMethod.ownerClass(),"Reaching method's owner class by ancestor's method.");

	this.callAncestorMethod();
	
	equals(this.Ancestor.prototype.privateMethod,this.currentMethod,"Reaching current method from ancestor's method.");
	equals(this.Ancestor,this.currentClass,"Reaching current class from ancestor's method.");
	ok(!this.superClass,"Reaching empty super class from ancestor's method.");
	ok(!this.superMethods,"Reaching empty super proto from ancestor's method.");
	
	this.createDescendantMethods();

	equals(this.Descendant,this.Descendant.prototype.privateMethod.ownerClass(),"Reaching private method's owner class by descendant's method.");
	equals(this.Ancestor,this.Descendant.prototype.commonMethod.ownerClass(),"Reaching common method's owner class by descendant's method.");
	
	this.callDescendantMethod();
	
	equals(this.Descendant.prototype.privateMethod,this.currentMethod,"Reaching current method from descendant's method.");
	equals(this.Descendant,this.currentClass,"Reaching current class from descendant's method.");
	equals(this.Ancestor,this.superClass,"Reaching super class from descendant's method.");
	equals(this.Ancestor.prototype,this.superMethods,"Reaching super proto from descendant's method.");
	
	this.callAncestorMethod();
	this.saveCurrentToBenchmark();
	this.callAncestorMethodFromDescendantMethod();
	
	equals(this.benchmark.currentMethod,this.currentMethod,"Reaching current method from ancestor's method called descendant's method.");
	equals(this.benchmark.currentClass,this.currentClass,"Reaching current class from ancestor's method called descendant's method.");
	equals(this.benchmark.superClass,this.superClass,"Reaching super class from ancestor's method called descendant's method.");
	equals(this.benchmark.superMethods,this.superMethods,"Reaching super proto from ancestor's method called descendant's method.");
},
saveCurrent: function (currentMethod,currentClass,superClass,superMethods)
{
	this.currentMethod=currentMethod;
	this.currentClass=currentClass;
	this.superClass=superClass;
	this.superMethods=superMethods;
	return this;
},
withoutCurrentMethod: function ()
{
	var o={};
	this.saveCurrent(o.currentMethod(),o.currentClass(),o.superClass(),o.superMethods());
},
createAncestorMethods: function ()
{
	var test=this;
	this.Ancestor.append(
	{
		privateMethod: function ()
		{
			test.saveCurrent(this.currentMethod(),this.currentClass(),this.superClass(),this.superMethods());
		},
		commonMethod: function ()
		{
			test.saveCurrent(this.currentMethod(),this.currentClass(),this.superClass(),this.superMethods());
		}
	});
},
callAncestorMethod: function ()
{
	this.ancestor1.privateMethod();
},
createDescendantMethods: function ()
{
	var test=this;
	this.Descendant.append(
	{
		privateMethod: function ()
		{
			test.saveCurrent(this.currentMethod(),this.currentClass(),this.superClass(),this.superMethods());
		},
		ancestorCallerMethod: function ()
		{
			this.superMethods().privateMethod.call(this);
		}
	});
},
callDescendantMethod: function ()
{
	this.descendant.privateMethod();
},
saveCurrentToBenchmark: function ()
{
	this.benchmark={};
	this.saveCurrent.call(this.benchmark,this.currentMethod,this.currentClass,this.superClass,this.superMethods);
},
callAncestorMethodFromDescendantMethod: function ()
{
	this.descendant.ancestorCallerMethod();
}

A kód szerintem magáért beszél, úgyhogy nem is részletezem.

Class bővítése

A megvalósításban a Class bővítése a legegyszerűbb. Letároljuk az ős osztály értékét, a Class.superClassben pedig visszakérjük azt.

Class.prototype=
{
	//...
	extend: function (o)
	{
		//...
		Descendant.__SUPER__=Ancestor;
		//...
	},
	superClass: function ()
	{
		return this.__SUPER__;
	},
	superMethods: function ()
	{
		var superClass=this.superClass();
		return superClass && superClass.prototype;
	}
};

Function bővítése

A metódusok osztályhoz kötése ugyanúgy megy, mint az előzőekben a Class bővítésénél. Annyi extra van, hogy ellenőrizzük, hogy a metódus még nincs osztályhoz kötve.

Function.prototype.ownerClass=function ()
{
	return this.__CLASS__;
};

//...

Class.prototype=
{
	append: function (o)
	{
		if (!o)
		{
			return this;
		}
		for (var j in o)
		{
			if (!o.hasOwnProperty(j))
			{
				continue;
			}
			var v=o[j];
			if (v instanceof Function)
			{
				if (v.constructor===Class)
				{
					throw new TypeError("Class property cannot be a Class.");
				}
				else if (v.__CLASS__)
				{
					throw new SyntaxError("Method already owned by another class.");
				}
				else
				{
					v.__CLASS__=this;
				}
			}
			this.prototype[j]=v;
		}
		return this;
	},
	//...
};

Object bővítése

A legnehezebb része a megvalósításnak, hogy hogyan juttassuk el az aktuális példányhoz, hogy éppen melyik metódust hívták meg rajta.

A metódus törzsében az arguments.calleeval lehet elérni magát a metódust. Az Object.currentMethodnál ugyanezt az arguments.callee.callerrel lehetne megtenni. Ennek a megoldásnak több hátránya van. Az egyik, hogy nem cross-browser, a másik, hogy ha az Object.currentClassből szeretnénk lekérni, hogy mi az aktuális metódus, akkor az Object.currentClasst kapnánk. Szóval nem ez a járható út.

Keressünk egy jobb megoldást. A Function.ownerClass megvalósításánál fentebb a metódus egy tulajdonságának adtunk értéket. Ugyanezt kéne megtennünk az aktuális példánnyal az aktuális metódus futásának az idejére. Így az Object bővítése elég egyszerűen menne.

Object.prototype.currentMethod=function ()
{
	return this.__METHOD__;
};
Object.prototype.currentClass=function ()
{
	var currentMethod=this.currentMethod();
	return currentMethod && currentMethod.ownerClass();
};
Object.prototype.superClass=function ()
{
	var currentClass=this.currentClass();
	return currentClass && currentClass.superClass();
};
Object.prototype.superMethods=function ()
{
	var superClass=this.superClass();
	return superClass && superClass.prototype;
};

Metódusok díszítése

Hogy egy változót csak a metódus futásának az idejére írjunk felül, csak bonyolultabb eszközökkel lehet megoldani. A legismertebb példa a témában a Function.bind().


Function.prototype.bind=function (context)
{
	var callee=this;
	return function ()
	{
		return callee.apply(context,arguments);
	};
};

Elemezzük ezt egy kicsit. A kontextus az a változó, amit a függvény futásának az idejére felülírtunk. Ha úgy tekintünk a függvényekre, mint objektumokra, akkor a bind()-ról az alábbiakat mondhatjuk el:

  • Az aktuális függvény és a kötött függvény is ugyanazt a felületet valósítja meg, hiszen mindketten a Function natív osztály példányai.
  • Lehetőség van a kötött függvény felületének bővítésére, de mivel a kötött függvényt továbbra is függvényként akarjuk használni, ezért az azokra jellemző viselkedést meg kell tartania.
  • A kötött függvénynek bővült a felelősségi köre az eredeti függvényhez képest, hiszen a futás idejére a kontextus beállítása is az ő feladata lett.

Ezek a tulajdonságok a díszítő (decorator) minta jellemzői. Tehát ha egy függvényhez új felelősségkört akarunk a futás idejére hozzárendelni akkor javascriptben díszítenünk kell. Ennek az az oka, hogy a függvények hívására vonatkozó natív kódba nem nyúlhatunk bele. (A díszítésen kívül még lehetséges a függvény törzsének szövegként való feldolgozásával bővített függvény létrehozni, de ez nem tanácsos, mert az eredeti függvény scope-ja teljesen más lesz, mint az újonnan létrehozotté.)

A metódusainkra ugyanaz vonatkozik, mint a függvényekre, ezért a metódusainkat díszítenünk kell, és a prototípusba a díszített metódusnak kell bekerülnie. Mivel a prototípushoz a ClassBuilder.append()-del adunk minden esetben metódust, ezért a díszítést is itt kell elvégezni. A jobb átláthatóság érdekében érdemes a metódusokra is ugyanúgy valamilyen létrehozási (creation) mintát használni, mint az osztályok létrehozásánál tettük. Mivel nincs több metódus változat, ezért egy sima gyár (factory) megteszi.

Először emeljük ki a MethodFactory.createMethod()-be a ClassBuilder.append()-ből a metódusokra vonatkozó kódot.

var MethodFactory=
{
	createMethod: function (ownerClass,callee)
	{
		if (callee.constructor===Class)
		{
			throw new TypeError("Cannot create a method from a class.");
		}
		if (callee.__CLASS__)
		{
			throw new SyntaxError("Method already owned by another class.");
		}
		callee.__CLASS__=ownerClass;
		return callee;
	}
};

//...

Class.prototype=
{
	append: function (o)
	{
		if (!o)
		{
			return this;
		}
		for (var j in o)
		{
			if (!o.hasOwnProperty(j))
			{
				continue;
			}
			this.prototype[j]=(o[j] instanceof Function)
				?MethodFactory.createMethod(this,o[j])
				:o[j];
		}
		return this;
	},
	//...
};
Ezután a MethodFactory már egyszerűen bővíthető a díszítés folyamatával.

var MethodFactory=
{
	createMethod: function (ownerClass,callee)
	{
		if (callee.constructor===Class)
		{
			throw new TypeError("Cannot create a method from a class.");
		}
		if (callee.__CLASS__)
		{
			throw new SyntaxError("Method already owned by another class.");
		}
		var method=function ()
		{
			var caller=this.__METHOD__;
			this.__METHOD__=arguments.callee;
			var result=callee.apply(this,arguments);
			this.__METHOD__=caller;
			return result;
		};
		method.__CLASS__=ownerClass;
		method.toString=function ()
		{
			return callee.toString();
		};
		return method;
	}
};

A toString()-et is átállítottam, hogy a díszített függvény kódját adja vissza, ne pedig a díszítőét. Felesleges további részfolyamatokra bontani a metódusok létrehozását, így is elég átlátható a dolog.

Class.toString

Futtassuk a tesztet. Internet Explorer 8 nálam nem szereti, ennek az az oka, hogy hiányolja a Class.toString()-et, azért, mert a Function.toString()-et kitöröltük az osztályok létrehozásánál. Itt az ideje valamit tenni ez ügyben. Bővítsük a Class.prototype-ot a saját toString()-gel, és ClassBuilderben állítsuk be az értéket manuálisan, ugyanis MSIE for-in ciklusnál hajlamos átugrani a toString()-et.

var ClassBuilder=
{
	//...
	implementClassInterface: function ()
	{
		this.current.constructor=Class;
		for (var j in Class.prototype)
		{
			if (Class.prototype.hasOwnProperty(j))
			{
				this.current[j]=Class.prototype[j];
			}
		}
		this.current.toString=Class.prototype.toString;
	},
	//...
};

//...

Class.prototype=
{
	//...
	toString: function ()
	{
		return "class {}";
	}
};

Így már megy abban a böngészőben is.

Natív osztályok

A natív osztálynál nincs értelme örököltetni, szóval inkább töröljük az extend()-et ezeknél.

Bővítsük a tesztünket:


ok(!Array.extend,"Native class inheritance disabled.");

Csináljunk egy ClassBuilder.disableInheritance() metódust, amit csak a natív osztályok létrehozásánál hívjunk meg.


var ClassBuilder=
{
	//...
	disableInheritance: function ()
	{
		delete(this.current.extend);
	},
	//...
};

//...

var ClassDirector=
{
	createClass: function ()
	{
		ClassBuilder.newClass();
		ClassBuilder.removeFunctionInterface();
		ClassBuilder.implementClassInterface();
		return ClassBuilder.getResult();
	},
	nativeClass: function (nativeClass)
	{
		ClassBuilder.setClass(nativeClass);
		ClassBuilder.removeFunctionInterface();
		ClassBuilder.implementClassInterface();
		ClassBuilder.disableInheritance();
		return ClassBuilder.getResult();
	}
};

Ezzel a ClassDirector is elég sokat módosult. (A ClassDirector az előző cikkben véletlenül megmaradt ClassFactory néven az építő (builder) mintára váltáskor.)

Futtassuk a tesztet. Ezzel a végére értünk az öröklés témakörének.

A cikk illusztrációjához Paula Baile fotóját használtuk fel.

 
1

Kicsit hosszúra sikeredett, a

inf · 2010. Okt. 26. (K), 13.37
Kicsit hosszúra sikeredett a cikk. Bocsi. :-)

A github-os változatba betettem még egy olyan rendszert, ami véd attól, hogy prototípusra hívjunk meg metódusokat. Én gyakran elkövettem azt a hibát, hogy

this.superMethods().initialize();
ahelyett, hogy

this.superMethods().initialize.call(this);
Az első változat pedig nem túl szerencsés, mert ha olyan az initialize, akkor a prototípus módosulásához vezet.


Ha valaki esetleg tud olyan tesztet, amivel egyszerűen meg lehet mondani, hogy mi prototípus és mi nem, akkor szóljon. Én azt csináltam, hogy a prototípusban beállítottam egy __IS_PROTO__ nevű változót, amit aztán a példányosításkor false értékre írtam felül.
A natív osztályoknál nincs lehetőség öröklésre, ezért ők nem kaptak ilyen biztonsági rendszert.