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