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
- Egy JavaScript keretrendszer születése – bevezetés
- Egy JavaScript keretrendszer születése – natív osztályok
- Egy JavaScript keretrendszer születése – öröklődés
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ő.
■
Class lesz, csak így tovább!
(bocsánat az idióta szójátékért, hajnalban előfordul)
np
Hoppá! Szóval megszületett.
nagyon tetszett
Kösz a dicséreteket. TDD-t
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
Sajnos mégsem lett elég
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)
király
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.
Csupán ehhez a kódhoz elég
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
Szerintem ugy erti, hogy uj
Igen, tudom, hogy lehet