ugrás a tartalomhoz

Egy JavaScript keretrendszer születése – bevezetés

inf3rno · 2010. Szep. 20. (H), 15.26
Egy JavaScript keretrendszer születése – bevezetés

Jelen cikksorozatban egy javascriptes keretrendszer alapjait fektetem le. Konkrétan arról lesz szó többek között, hogy hogyan lehet JavaScriptben használhatóbbá tenni az osztályokat, csomagokat létrehozni, és egyszerűsíteni a típusellenőrzést.

A sorozatban megjelent

Azért döntöttem sorozat írása mellett, mert egész egyszerűen túl nagy ez a téma ahhoz, hogy egy cikkben foglaljam össze. Gondolom nektek is elég fárasztó lenne egyszerre 50 oldalt olvasni ugyanarról a témáról, nem is beszélve arról, hogy csak az end gombbal lehetne a commentekhez jutni, én meg csak görgőzni szeretek. :-)

A sorozatban bemutatott kódok természetesen tetszés szerint módosíthatóak és bővíthetőek.

Vágjunk is bele!

Osztályok JavaScriptben

Alap járaton JavaScriptben a konstruktort adjuk meg. A konstruktort a new kulcsszóval használva példányosíthatjuk. A konstruktor prototype tulajdonságában érhető el az osztályhoz tartozó prototípus. A JavaScript ezt használja az összes példánynál alapként. Ha egy példányban egy változót ettől függetlenül állítunk be, akkor az elfedi a prototípusban megadott változót az adott példánynál. Ha egy változónak a prototípusban függvényt adunk meg értéknek, akkor az az osztály metódusa lesz. Az osztály metódusaiban és a konstruktorban a this kulcsszó az aktuális példányra mutat.

var MyClass_Constructor=function (number)
{
	alert("Ez a konstruktor függvény.");
	if (number!==undefined)
	{
		this.setNumber(number);
	}
	alert("A példányban tárolt szám: "+this.getNumber());
};
MyClass_Constructor.prototype.number=1;
MyClass_Constructor.prototype.setNumber=function (number)
{
	this.number=number;
};
MyClass_Constructor.prototype.getNumber=function ()
{
	return this.number;
};


var MyClass_Instance_1=new MyClass_Constructor(2);//A példányban tárolt szám: 2
var MyClass_Instance_2=new MyClass_Constructor();//A példányban tárolt szám: 1

Ez a szerkezet így nagyon szépen működik, viszont engem két dolog zavar benne.

Az egyik, hogy az osztályok nem válnak el igazán a függvényektől/metódusoktól, hiszen a konstruktor függvény segítségével hozunk létre osztályt. Ebből adódik az is, hogy ha egy változó függvényt tárol, akkor nem lehet eldönteni, hogy konstruktorról (osztályról) vagy csak egy szimpla függvényről van-e szó.

A másik zavaró dolog, hogy a példányváltozókat és a -metódusokat csak egyesével tudjuk hozzáadni az osztályhoz. Így minden alkalommal ki kell írni a konstruktor nevét és a prototype kulcsszót.

Új osztály létrehozása egyszerűbben – felület tervezése

A fenti problémákat a következő példában használt felülettel orvosoltam:

var MyClass=new Class(
{
	initialize: function (number)
	{
		alert("Ez a konstruktor függvény.");
		if (number!==undefined)
		{
			this.setNumber(number);
		}
		alert("A példányban tárolt szám: "+this.getNumber());
	},
	number: 1,
	setNumber: function (number)
	{
		this.number=number;
	},
	getNumber: function ()
	{
		return this.number;
	}
});

var MyClass_Instance_1=new MyClass(2);
var MyClass_Instance_2=new MyClass();

Ami feltűnhet, hogy a new Class()-szal kapott osztályt a példányosításnál szintén a new kulcsszóval hívtam meg. Ez persze csak akkor történhet meg, ha a new Class() egy konstruktort ad vissza. Ez JavaScriptben lehetséges, ugyanis ha a new kulcsszóval meghívott függvénynek visszatérő értéket állítunk be, akkor a kifejezés értéke a visszatérő érték lesz, és nem az újonnan létrehozott példány.

var Class=function ()
{
	return "Visszatérő érték.";
};

alert(new Class());//Visszatérő érték.

A másik dolog, ami feltűnhet, hogy az initialize változóban tároltam le a konstruktort. (Azért esett a választásom erre a szóra, mert gyakorlatilag az összes JavaScriptes keretrendszer ezt használja.) Ezzel a megoldással nem kötelező konstruktort megadnunk, ha amúgy is üres lenne. Ezt a kényelmi funkciót nyilván beépítjük majd az implementációba (megvalósításba).

Egységtesztelés

Itt álljunk meg egy pillanatra. Most már van egy használható felületünk, amit majd a későbbiekben továbbfejleszthetünk. Tehát már érdemes elkezdeni írni a teszteket hozzá. A teszteléshez QUnitot használtam. A telepítése elég egyszerű, le kell szedni a JQuery/Qunit oldalról a qunit.js-t és a qunit.css-t. Ezután kell csinálni egy HTML fájlt, ami a megjelenítésért felel, meg persze beszúrja a tesztjeinket. (Ezt én is csak copy-paste-eltem, elnézést, hogy nem lett XHTML strict.)


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
	<head>
		<link rel="stylesheet" href="qunit.css" type="text/css"/>
		<script type="text/javascript" src="qunit.js"></script>
		<script src="class.js"></script>
		<script src="test.class.js"></script>
	</head>
	<body>
		<h1 id="qunit-header">QUnit example</h1>
		<h2 id="qunit-banner"></h2>
		<h2 id="qunit-userAgent"></h2>
		<ol id="qunit-tests"></ol>
	</body>
</html>

A fejlesztés során először felületet tervezünk, aztán teszteket írunk, és csak utána implementálunk. (A felület tervezés alatt az osztály felületét értem. A cikkbe nem vettem bele az interface-eket.) A fejlesztés során csak kis lépésenként haladunk, hogy ne legyen annyira monoton.

Új osztály létrehozása – teszt írás

Az új osztály tesztjénél egyrészt meg kell nézni, hogy tényleg osztályt kapunk-e vissza, másrészt hogy a visszakapott osztály prototípusában bent vannak-e a paraméterben megadott dolgok. Szükség van továbbá annak az ellenőrzésére is, hogy a példányosításnál meghívja-e a konstruktor az initialize metódust, illetve, hogy az initialize hiányában történik-e valami.

Első körben hozzuk létre a test.class.js-t, aztán adjuk meg a modul nevét, és hozzuk létre az új osztály tesztet.

module("class.js");

test("create new class",function ()
{
	var param=
	{
		v: {},
		s: "default",
		initialize: function (s)
		{
			this.s=s;
		}
	};
	var TestClass=new Class(param);
	equals("function",typeof(TestClass),"Constructor returned.");
	equals(param.v,TestClass.prototype.v,"Variables are in the prototype.");
	
	var instance=new TestClass("test");
	equals("test",instance.s,"Initialize called after instance creation.");
	
	var EmptyClass=new Class({});
	var empty=new EmptyClass();
	ok(!!empty,"Create and use class without initialize.");
});

Ha most futtatjuk a tesztet, akkor a következőt kapjuk: Died on test #1: Class is not defined. Ugye a Class globális változó még üres, és ezért hibával meghalt a tesztünk. Dobjuk össze most a class.js-ben a Class függvényt.

Mit kell tudnia a Class függvénynek?

  • Ellenőriznie kell, hogy new kulcsszóval hívták-e
  • Létre kell hoznia egy új konstruktort
  • Törölnie kell a Function.prototype-ból örökölt dolgokat
  • Hozzá kell adnia az osztályokhoz tartozó függvényeket
  • Bővíteni kell az osztály prototípusát a paraméterben található értékekkel

Ez nagyon sok feladat lenne egy függvénynek, ezért aztán érdemes külön singletont létrehozni, ami tudja kezelni ezeket a funkciókat. Mivel egy bonyolult dolgot építünk fel, azért a builder pattern a célravezető. A buildert indokolja az is, hogy vannak natív osztályok, melyekhez később szintén hozzá kell adni az osztályokra jellemző viselkedést.

(function ()
{
	var isKey=function (o,k)
	{
		return (k in o) && (!(k in Object.prototype) || Object.prototype[k]!==o[k]);
	};

	var ClassBuilder=
	{
		classMethods:
		{
			append: function (o)
			{
				if (!o)
				{
					return this;
				}
				for (var i in o)
				{
					if (!isKey(o,i))
					{
						continue;
					}
					this.prototype[i]=o[i];
				}
				return this;
			}
		},
		newClass: function ()
		{
			this.current=function ()
			{
				if (this.constructor!==arguments.callee)
				{
					throw new SyntaxError("Just like new Class(...), not like Class.apply(...) or Class(..)");
				}
				if (this.initialize instanceof Function)
				{
					this.initialize.apply(this,arguments);
				}
			};
		},
		clearFunctionMethods: function ()
		{
			for (var i in this.current)
			{
				if (!isKey(this.current,i))
				{
					continue;
				}
				if ((i in Function.prototype) && Function.prototype[i]===this.current[i])
				{
					this.current[i]=undefined;
				}
			}
		},
		addClassMethods: function ()
		{
			for (var i in this.classMethods)
			{
				
				if (!isKey(this.classMethods,i))
				{
					continue;
				}
				this.current[i]=this.classMethods[i];
			}
		},
		getResult: function ()
		{
			var result=this.current;
			delete(this.current);
			return result;
		}
	};
	
	var Class=function (o)
	{
		if (this.constructor!==arguments.callee)
		{
			throw new SyntaxError("Just like new Class(...), not like Class.apply(...) or Class(..)");
		}
		
		ClassBuilder.newClass();
		ClassBuilder.clearFunctionMethods();
		ClassBuilder.addClassMethods();
		var currentClass=ClassBuilder.getResult();

		currentClass.append(o);

		return currentClass;
	};
	
	var Global=Function("return this;")();
	Global.Class=Class;
})();

Hát itt sok dolog említésre méltó. Egyrészt külön closure-ba szerveztem a kódot, hogy global scope-ba (globális névtér) csak az oda tartozó dolgok menjenek ki, másrészt a Farkas Máté által javasolt módszert használtam a globális névtér elérésére.

A new kulcsszó használatát az arguments.callee és a this.constructor azonosságával ellenőrzöm. Persze ez nem tökéletes megoldás a témára, de jobb módszerről nem tudok.

Az isKey() függvény még ami magyarázatra szorul: azt ellenőrzi, hogy for in ciklusnál az adott tulajdonságot nem örökölte-e az Object.prototype-tól az adott objektum.

Futtassuk a tesztet. Nálam minden okés, nálatok?

A keretrendszer forráskódja letölthető.

 
inf3rno arcképe
inf3rno
Jánszky László JavaScripttel 1997 óta, PHP-vel 2005 óta foglalkozik, ismereteit autodidakta módon szerezte.
1

Class lesz, csak így tovább!

prom3theus · 2010. Szep. 21. (K), 03.27
Class lesz, csak így tovább! :)
(bocsánat az idióta szójátékért, hajnalban előfordul)
2

np

inf3rno · 2010. Szep. 21. (K), 13.35
Class az lesz :-D, meg úgy döntöttem, hogy bizonyos dolgokhoz Interface is kell, szóval mégiscsak benne lesz. Az alap rendszer 5 fejezet (class,package,inherit,interface,type) lesz, utána majd elgondolkodok, hogy van e értelme folytatni.
3

Hoppá! Szóval megszületett.

zhagyma · 2010. Szep. 21. (K), 14.15
Hoppá! Szóval megszületett. Így hirtelenjében: természetesen folytani kell. Tiszteletben tartva tervezői elképzeléseidet kíváncsian várom, hogy milyen hatást fognak gyakorolni a következő fejezetekben implementált kódokban. Eddig tetszik és a teszt hiba nélkül lefut.
4

nagyon tetszett

Crystal · 2010. Szep. 21. (K), 14.41
jó, cikk, örülök neki, hogy mostanában ilyen színvonalú írások vannak weblaboron. Azzal mondjuk valószínűleg lehetne vitatkozni, hogy érdemes-e a javascriptre klasszikus OO eszközöket "ráerőltetni", de ettől függetlenül kíváncsian várom a folytatást. Van a cikknek egy olyan "Practical introduction to TDD" jellege is, ez külön tetszett :)
5

Kösz a dicséreteket. TDD-t

inf3rno · 2010. Szep. 21. (K), 18.54
Kösz a dicséreteket. TDD-t most tanulom, aztán gondoltam nem árt gyakorolni egy kicsit :-) Eredetileg class diagramot is tákoltam hozzá, de úgy gondoltam mégsem teszem be, mert kóddal sokkal jobban be lehetett mutatni a kívánt viselkedést, meg mert azt is csak tanulom. Az első 5 fejezet után, amikor már tényleg osztályokkal dolgozunk, akkor teszek be diagramokat is.

Azzal mondjuk valószínűleg lehetne vitatkozni, hogy érdemes-e a javascriptre klasszikus OO eszközöket "ráerőltetni", de ettől függetlenül kíváncsian várom a folytatást.

Persze lehetne :-) Egyelőre ez a Class függvény szerintem még nem számít ráerőltetésnek, de később azért lesznek már olyan dolgok is. Van egy olyan trend, hogy ne szennyezzünk global namespace-t, meg hogy foglalt szavakat még nagy betűvel se használjunk, na ez az a része, amit teljesen figyelmen kívül hagytam. :-D Majd később lesz olyan, hogy beírok az Object.prototype-ba meg a Function.prototype-ba, meg persze global ns is bővülni fog. Már előre várom a megkövezést... :-D
6

Sajnos mégsem lett elég

inf3rno · 2010. Szep. 28. (K), 07.51
Sajnos mégsem lett elég színvonalas, van mit javítani... No sebaj, a hibákból tanul az ember.

Kitettem a javított kódokat:
github

Majd később átírom a cikkben is, ami lényeges, hogy builder helyett csak sima factory van, isKey helyett Object.hasOwnProperty az itteniek alapján és ennek hatására a clearFunctionMethods is egyszerűsödött.

(Érdekes, hogy ennyire egyszerű kódban is mennyi hibát el tudok követni :D)
7

király

Török Gábor · 2010. Szep. 28. (K), 11.52
A githubos kódtárolás nagyon zsír és praktikus. Azt javasolnám, hogy minden soron következő fejezethez nyiss új ágat, így könnyen áttekinthetők az egyes állomások, és könnyen betölthető az adott cikkhez tartozó állapot.

Másfelől én támogatnám, hogy mutasd be mindazokat, amiket rossz döntésnek tartottál, és amik helyett most más mintát választasz – mindenki okulására. Beck TDD könyvében is ebből tanultam a legtöbbet.
8

Csupán ehhez a kódhoz elég

inf3rno · 2010. Szep. 28. (K), 19.37
Csupán ehhez a kódhoz elég lenne egy factory, viszont a natív osztályoshoz már lehet, hogy jobb a builder. :-) Így tervezési mintákkal még nincs összerakva a natív osztályos kód, meg amikor megírtam, akkor még tdd-t sem használtam. Szóval most az lesz, hogy kibővítem natív osztályokkal a kódot, aztán ha adsz rá lehetőséget, akkor a cikket is kiegészítem ezzel a résszel.

Utána meg már jöhet az öröklődés, közben azt is átgondoltam, ott a 2*decorator és a decorator+strategy között vacilláltam. Még mindig van 1-2 kérdőjeles része, mert bele fogok tenni 1-2 új dolgot ahhoz a rendszerhez képest, amit most használok. Aztán az újításoknál meg csak a gyakorlati használat, ami alapján el lehet dönteni, hogy jók e vagy sem.


Köszi a githubos ötletet, tényleg zsír, bár az elején voltak félelmeim vele kapcsolatban, mert nem vagyok annyira otthon a verziókezelőkben. Igazából csak a commit-et nyomkodom, aztán majd lesz valami :D
9

Szerintem ugy erti, hogy uj

Ajnasz · 2010. Okt. 6. (Sze), 15.29
Szerintem ugy erti, hogy uj fejezethez keszits uj branchet az elozo fejezethez tartozo kodbol leaagazva. :] Igy azt is megcsinalhatod, hogy harom uton indulsz el (harom branch) es az mind lekovetheto.
10

Igen, tudom, hogy lehet

inf3rno · 2010. Okt. 7. (Cs), 00.35
Igen, tudom, hogy lehet ilyet, de inkább nem ágaztatom el, ha nem gond.