Vissza az előzőleg látogatott oldalra (nem elérhető funkció)Vissza a tananyag kezdőlapjára (P)Ugrás a tananyag előző oldalára (E)Ugrás a tananyag következő oldalára (V)Fogalom megjelenítés (nem elérhető funkció)Fogalmak listája (nem elérhető funkció)Oldal nyomtatása (nem elérhető funkció)Oldaltérkép megtekintése (D)Keresés az oldalon (nem elérhető funkció)Súgó megtekintése (S)

Modern programozási minták a kliens és szerveroldali webprogramozásban / Kódszervezés és modularitás

Tanulási útmutató

Összefoglalás

Ez a fejezet abba nyújt betekintést, hogy JavaScriptben milyen lehetőségek vannak a kód modularizáltabbá tételére.

Kódszervezés és modularitás

Kódszervezési koncepciók általában

Ahhoz, hogy egy alkalmazás áttekinthető, karbantartható legyen, valamilyen módon tagolni szükséges. A tagolás módja függ az alkalmazás méretétől. Másképpen kell hozzáfogni egy kis alkalmazásnál, és megint más módszereket kell alkalmazni több ezer sor írásakor. Sokszor nem lehet előre megtervezni a tagolás mikéntjét, hanem a kód növekedésével kell a megfelelő kódszervezési technikákat alkalmazni. Érdemes a fejlesztéseinkben ezt a folyamatosságot alkalmazni, mert így egyrészt nem köti gúzsba a kezünket egy elején hozott döntés, másrészt a cél határozza meg az eszközt, és nem az eszköz határozza meg a kódolási stílusunkat.

Függvény

A függvények elsődleges feladata, hogy egy adott feladatot elvégző kódrészletet strukturálisan elválasszanak a többi kódtól, ráadásul a paraméterezésen keresztül az adott feladat megfelelőképpen általánosítható.

Kis méretű alkalmazások strukturálására elegendőek a függvények. A függvények alkotják végeredményében az alkalmazás építőelemeit, az általuk képviselt részfeladatok megfelelő sorrendben történő végrehajtásával áll össze a teljes feladat megoldása.

A kódot nagyon sokféleképpen lehet függvényekre osztani. Az alábbiakban felsorolunk pár olyan irányelvet, amely segít ennek a feladatnak az elvégzésében:

Osztály

A függvények adatokkal dolgoznak. Az osztályok alkalmazásával lehetővé válik, hogy egy adott funkcionalitás elvégzéséhez szükséges adatokat és az azokat feldolgozó függvényeket elkülönítsük a többitől. A klasszikus objektum-orientált nyelvekben az osztályok ennél többet is elvégeznek. A publikus adatokon és metódusokon keresztül képes definiálni egy objektum külvilág felé mutatott arcát, ez az objektum interfésze, a megvalósítás belügyeit viszont képes elrejteni a külvilág elől (egységbe zárás).

Modul

Hasonló feladat elvégzésére szolgálnak, de koncepcionálisan más csoportba tartoznak a modulok. A modul egy adott funkcionalitást megvalósító független egység, amely egy alkalmazás egészének egy részét alkotja. A modulok újrahasznosítható szoftver komponensek, amelyek egy alkalmazás építőelemeit alkotják. A modulok nagy előnye, hogy definiálják az interfészüket, amelyen keresztül az adott funkcionalitás elérhető. Így a megvalósítás részletei rejtve maradnak, és később az interfész módosítása nélkül is alakíthatók. Meghatározott interfészek mentén sokkal jobb dolgozni, mint egy adott implementációhoz igazodni.

A modularitás alapelvei szerint egy modulnak a következő feltételeket kell teljesítenie:

Egy modul esetében az egyik legfontosabb dolog az interfésze, amit a külvilág felé felhasználásra biztosít. Tulajdonképpen egy szerződés a program többi komponense felé arról, amit megvalósít. Az interfészek tervezésére tehát különösen oda kell figyelni. Ezzel kapcsolatban is jól meghatározható néhány alapelv.

A legtöbb programozási nyelvnek van saját modulkezelő rendszere. Ilyenek pl. C++-ban a függvénykönyvtárak, amelyeket az #include-dal emelünk be, vagy a Pascalbeli unit-ok, amiket a uses kulcsszó emel be, vagy a Javabeli import kulcsszó tartozhat még ide.

Végül utolsó megjegyzésként megemlíthető, hogy a modul hasonló viselkedést mutat az osztállyal annyiban, hogy az osztály is egy adott funkcionalitáshoz tartozó működést foglalja egységbe, működtetéséhez pedig megfelelő publikus interfészt biztosít. A modulok ezért sokszor egy-egy osztályt tartalmaznak. De a modulok annyiban többek, hogy általában fájlszintű szeparációt is biztosítanak, azaz egy modul egy fájl, és meghatározható a többi modultól való függőségük is.

Vissza a tartalomjegyzékhez

Kódszervezés JavaScriptben

JavaScript nem bővelkedik a kódszervezést segítő nyelvi elemekben. JavaScriptben nincsenek osztályok, csak objektumok, és az objektumok tulajdonságainak hozzáférhetőségét sem lehet beállítani. Ugyancsak nincsen külön modulkezelő rendszere. JavaScriptben a kódszervezésre két nyelvi elem, az objektumok és a függvények szolgálnak, ezek viszont vannak annyira rugalmasak, hogy velük a fentebbi koncepciók jól megvalósíthatóak.

Függvények

A kód alapvető strukturálására természetesen JavaScriptben is a függvények szolgálnak. Függvények írása már csak azért is elkerülhetetlen, mert az aszinkron környezet jellegéből adódóan függvényekbe kell ágyaznunk bizonyos események bekövetkeztekor lefutó logikát. Az callback és eseménykezelő függvényeken kívül azonban a kód többi részét is érdemes a tiszta kód szabályainak megfelelően minél több részre tagolni.

Forráskód
var elems = [],
    push = function (e) {
        elems.push(e);
    },
    pop = function () {
        return elems.pop();
    },
    top = function () {
        return elems[size()-1];
    },
    size = function () {
        return elems.length;
    };

Objektumok

A kódban definiált adatok és függvények hamar ellephetik a globális névteret, ráadásul semmilyen nyelvi elem sem biztosítja az adott funkcionalitáshoz tartozó adatok és függvények összetartozását. Az összetartozó adatokat és metódusokat objektumliterállal foghatjuk össze. Ekkor az objektum neve meghatározza a funkcionalitást, és rajta keresztül elérhetők a megfelelő metódusok és adatok.

Forráskód
var stack = {
    elems: [],
    push: function (e) {
        this.elems.push(e);
        return this;
    },
    pop: function () {
        return this.elems.pop();
    },
    top: function () {
        return this.elems[this.size()-1];
    },
    size: function () {
        return this.elems.length;
    }
};
stack.push(10).push(20);
ok( stack.top() === 20, 'Az utoljára betett elem van a tetején');
ok( stack.size() === 2, 'A mérete is megfelelő');
ok( stack.elems.join(',') === '10,20', 'Az elemtömb kívülről is elérhető.');

A fenti példából is jól látható, hogy az objektumliterállal nem rejthetőek el az implementációs részletek. Egy másik probléma pedig az, hogy nincs lehetőség a modul létrehozásakor inicializáló kódot futtatni, csak úgy, ha egy külön függvénybe tesszük ezt a kódot (pl. init()), és a modul létrehozása után lefuttatjuk.

Névterek

Az objektumliterálos megközelítés egyik továbbfejlesztett változata az objektumok névterekbe szervezése. A JavaScript nyelvben nincsenek beépített névterek, de ezek jól szimulálhatóak egymásba ágyazott objektumokkal. Így a globális névtérbe felvett objektumok sokasága helyett tetszőleges objektum-, vagy modulhierarchia építhető fel. Ezzel az is elérhető, hogy alkalmazásunk csak egy globális objektummal bővítse a névteret, és minden egyéb funkcionalitást ez alá szervezzünk.

Forráskód
var myApp = myApp || ;
myApp.dataStructures = myApp.dataStructures || ;
myApp.dataStructures.stack = {
    elems: [],
    push: function (e) { /* ... */ },
    pop:  function () { /* ... */ },
    top:  function () { /* ... */ },
    size: function () { /* ... */ }
};

A fenti kódrészletben látható, hogy a felülírás elkerülése végett mindig ellenőrizzük, hogy az adott objektum létezik-e, s ha nem, akkor létrehozzuk. Névterek létrehozására bevezethetünk egy namespace() segédfüggvényt, mely nagyobb mélységek létrehozásában segíthet minket.

Forráskód
var MyApp = MyApp || ;
MyApp.namespace = function (ns) {
    var parts = ns.split('.'),
        parent = MyApp,
        i;
    //Ha MyApp-pal kezdődik, akkor kihagyható
    if (parts[0] === "MyApp") {
        parts = parts.slice(1);
    }
    for (i = 0; i < parts.length; i += 1) {
        //Nem létező tulajdonság létrehozása
        if (typeof parent[parts[i]] === "undefined") {
            parent[parts[i]] = ;
        }
        parent = parent[parts[i]];
    }
    return parent;
};

Ezzel a fenti névtér létrehozása a következőképpen alakul:

Forráskód
var stack = MyApp.namespace('MyApp.dataStructures.stack');
stack = { /* ... */ };
//vagy
MyApp.namespace('dataStructures.stack') = { /* ... */ };

Modul minta

Az objektumliterálos kódszervezés hiányát, miszerint az implementáció részletei is publikusak, a modul minta próbálja orvosolni. JavaScriptben csak a függvények adnak hatókört, így ha azt szeretnénk, hogy a modulunk kódja elkülönüljön és külön hatókörben fusson le, akkor a funkcionalitást függvénybe kell csomagolni. Ráadásul a függvény closure-t is definiál, amivel pedig a függvény törzsében lévő kódrészleteket elrejthetjük a külvilág elől.

A modul minta tehát mindig egy függvényből áll. A függvényen belül tetszőleges változókat és függvényeket deklarálhatunk. A modul interfészét a függvény visszatérési értéke határozza meg, ami általában egy objektum.

Forráskód
var module = (function () {
    //Inicializáló kód
    //Rejtett változók és függvények
    //Visszatérés egy objektummal
    return {
        //Publikus interfész
    };
})();

Verem példánk a modul mintával az alább látható. Lényeges különbség az objektumliterálos formával szemben, hogy a privát tagokra a this nélkül, míg a publikus tagokra a this kulcsszón keresztül kell hivatkozni.

Forráskód
var stack = (function () {
    var elems = [];
    return {
        push: function (e) {
            elems.push(e);
            return this;
        },
        pop: function () {
            return elems.pop();
        },
        top: function () {
            return elems[this.size()-1];
        },
        size: function () {
            return elems.length;
        }
    };
})();
stack.push(10).push(20);
ok( stack.top() === 20, 'Az utoljára betett elem van a tetején');
ok( stack.size() === 2, 'A mérete is megfelelő');
ok( stack.elems === undefined, 'Az implementáció rejtve van');

A modul minta osztályok emulálására is használatos, nem véletlen az objektumgenerátoroknál megfigyelt mintákkal való egyezés. Ott is és ebben az esetben is a privát láthatóságot egy függvény biztosítja a closure-ével, a publikus interfészt pedig a visszatérő objektum adja.

A modul minta variánsai

A modul mintának számos változata létezik. Ezek közül nézünk meg néhányat.

Névterek

Az eredeti mintában a függvény visszatérési értékét egy megadott névtér alá fűzték.

Forráskód
MyApp.namespace('dataStructures.stack') = (function () {
    /* ... */
})();

Globális változók importálása

Globális változókat az önkioldó függvény paramétereként lehet megadni. Így akár ugyanannak a függvénykönyvtárnak több különböző verziója is a modul hatókörébe injektálható.

Forráskód
var module = (function (win, doc, $, undefined) {
    /* ... */
})(window, document, jQuery);

Modul módosítása

Egy modul funkcionalitását akár több fájlba is szétszedhetjük. Ekkor a modul paraméterként kapja meg a módosítandó modult, és kiegészíti azt. Ha a betöltés sorrendje nem biztosítható, akkor meg kell vizsgálni a paraméterként átadandó objektumot, hogy létezik-e, és ha nem, akkor egy üres objektumot létrehozni. Az alábbi példa azt is megmutatja, hogy hogyan lehet a kiterjesztés során felülírt metódusokat továbbra is használni.

Forráskód
var module = (function (module) {
    var old_method = module.method;
    /* ... */
    return extendDeep(module, {
        //Kiegészítés, illetve felülírás
        method: function () {
            var old = old_method();
            /* ... */
        }
    })
})(module || );

Gyárfüggvény visszaadása

Sokszor célszerű nem egy objektumot, hanem egy függvényt, sőt egy gyárfüggvényt visszaadni. Ez történhet úgy, hogy az önkioldó függvényt egyszerű függvényre cseréljük, de úgy is, hogy az önkioldó függvényen belül definiált függvényt adja vissza az önkioldó függvény (ld. a gyárfüggvények becsomagolását az objektumokról szóló fejezetben).

Forráskód
var module = function () {
};
//vagy
var module = (function () {
    var Constr = function ()  {
        /* ... */   
    };
    return Constr;
})();

Felfedő modul minta

A felfedő modul minta az eredeti modul mintának azt a hiányosságát orvosolja, hogy másképpen kell hivatkozni a privát és a publikus adattagokra és metódusokra. Ebben a mintában minden adatot és metódust a függvény rejtett részében deklarálunk, majd a visszatérési értékként megadott objektum publikus interfészét képviselő tulajdonságaihoz rendeljük hozzá a rejtett adattagokat és metódusokat. A hivatkozás így mindig a this nélkül történik. Verem példánk a felfedő modul mintával így néz ki.

Forráskód
var stack = (function () {
    var elems = [],
        push = function (e) {
            elems.push(e);
            return this;
        },
        pop = function () {
            return elems.pop();
        },
        top = function () {
            return elems[size()-1];
        },
        size = function () {
            return elems.length;
        };
    return {
        push: push,
        pop: pop,
        top: top,
        size: size
    };
})();

Alkalmazásfüggetlen modul minta

A modul minta egy másik nagy hátránya, hogy alkalmazásával mindig bővül a globális névtér. Erre megoldásként a névterezés szolgál, de ebben az esetben pedig a modul definíciójába bekerül az alkalmazás névtere, így egy másik alkalmazásba nem lehet a modult átírás nélkül újra felhasználni.

Az általános megoldást az jelenti, hogy egy előre megadott exports nevű objektumot bővítünk a modulon belül, és a modulon kívül adjuk át az exports konkrét értékét. Ezzel a modult mindenféle körülménytől (globális objektumtól, névterektől) függetlenítettük, és a felhasználási helye dönti el, hogy mihez adja hozzá a modult. Ezt a mintát alkalmazzák a modern modulkezelő könyvtárak.

Forráskód
(function (exports) {
    /* ... */
    extendDeep(exports, {
        //Publikus interfész
    })
})(exports);

Homokozó minta (Sandbox)

A homokozó minta az alkalmazásfüggetlen modul mintához hasonlóan a globális névtérszennyezést próbálja orvosolni (gyakorlatilag ugyanazzal a módszerrel, miszerint egy függvényben várja el a megvalósítást), de ezen túl biztosít számára közös eszközöket úgy, hogy közben nem interferál a modul más modul megoldásaival.

Vissza a tartalomjegyzékhez

Modulkezelő könyvtárak

JavaScriptben a modulok egységes kezelésére tett kísérletek mára két kikristályosodott módszert eredményeztek: CommonJS és AMD. Mindkettő valamilyen módon visszavezethető az alkalmazásfüggetlen modul mintára, bár megoldásaik és felhasználási területük erőteljesen különböző. A két átmeneti megoldás mellé az utóbbi időszakban került a szabvány által előterjesztett modul minta, amely majd hosszútávon fogja beépített nyelvi elemekkel segíteni a modulkezelést.

Mindegyik modulkezelőnek két alapvető eleme van:

CommonJS

A Node.js megjelenése előtt már számos próbálkozás volt a JavaScript nyelv szerveroldali alkalmazására. Mindegyik próbálkozás kísérletet tett egy megfelelő modulrendszer kidolgozására. A különböző megközelítéseket a CommonJS szabvány próbálta közös nevezőre hozni. A CommonJS szabvány szerinti modul mintát a szerveroldali JavaScriptes környezetekben használják, és kiválóan alkalmas modulok szinkron betöltésére.

A CommonJS szabványnak igen egyszerű a szintaxisa. Minden modul külön fájlban helyezkedik, így nincs is szükség a modult körülvevő, hatókört biztosító függvényre. Minden fájl külön hatókörrel bír. A fájlon belül az exportálandó API-t az exports nevű objektumhoz kell fűzni.

Forráskód
//stack.js
var elems = [],
    push = function (e) { /* ... */ },
    pop = function () { /* ... */ },
    top = function () { /* ... */ },
    size = function () { /* ... */ };
extendDeep(exports, {
    push: push,
    pop: pop,
    top: top,
    size: size
});

Egy modult használni a require kulcsszóval lehet. Ez tölti be szinkron módon a kívánt modult, visszatérési értéke az, amit az exports objektumhoz hozzáfűztünk.

Forráskód
//app.js
var stack = require('./stack.js');
stack.push(10).push(20);

Aszinkron Modul Definíció – AMD

Kliensoldali környezetben CommonJS modulok a szinkron betöltésük miatt használhatatlanok. A szinkronitás egyrészt megállítaná a böngésző működését addig, amíg nem töltődne be az adott modul, ha pedig párhuzamosan töltenénk be a modulokat, akkor az egymástól való függőségük nem lenne megoldott. A böngészők környezetéhez igazított aszinkron modul kezelést az Aszinkron Modul Definíciós szabvány, röviden az AMD írja le.

Az AMD kulcseleme a modulok definiálásáért felelős define() függvény. Ennek első paramétere az opcionálisan megadható modulnév, amit általában nem adunk meg a modul nagyobb hordozhatósága érdekében (névtelen modulok). Második paramétere a modul függőségeit leíró tömb, amelyben az importálandó modulok nevei vannak felsorolva. Utolsó paramétere pedig egy függvény, mely a modul definícióját tartalmazza (ez a hatókört biztosító függvény). Ez a függvény explicit paraméterekként kapja meg a függőségi listában szereplő modulokat. A függvény interfészét visszatérési értékként kell megadni, akárcsak a klasszikus modul mintában.

Forráskód
define(modulnév, [modul1, modul2], function (modul1, modul2) {
    //Modul definíciója
    return {
        //Publikus API
    }
});

Vermünket a következőképpen definiálhatjuk AMD-ben:

Forráskód
define('stack', [], function () {
    var elems = [],
        push = function (e) { /* ... */ },
        pop = function () { /* ... */ },
        top = function () { /* ... */ },
        size = function () { /* ... */ };
    return {
        push: push,
        pop: pop,
        top: top,
        size: size
    };
});

Az alkalmazást egy require() függvény által megadott blokkban lehet elindítani. Ennek első paramétere a függőségi listát tartalmazza, második paramétere pedig hasonló módon kapja meg ezeket paraméterként. A függvény törzsébe az alkalmazás kezdőlépései kerülnek.

Forráskód
require(['stack'], function (stack) {
    stack.push(10).push(20);
});

Az AMD szabványt számos modulbetöltő függvénykönyvtár implementálta. Legnépszerűbbek:

CommonJS vs.AMD

UMD

EcmaScript 6 modulok

Az EcmaScript szabvány 6-os verziója kész megoldást próbál adni a modulok egységes kezelésére, mely egyaránt használható a szerveren és a böngészőben. Modul definiálására a module kulcsszó használatos. Modulon belül az import kulcsszóval tudjuk a függőségeinket megadni, a publikus interfészt az export kulcsszóval kell jelezni.

Az alábbi példa Addy Osmani online könyvéből való:

Forráskód
module staff{
    // specify (public) exports that can be consumed by
    // other modules
    export var baker = {
        bake: function( item ){
            console.log( "Woo! I just baked " + item );
        }
    }
}
module skills{
    export var specialty = "baking";
    export var experience = "5 years";
}
module cakeFactory{
    // specify dependencies
    import baker from staff;
    // import everything with wildcards
    import * from skills;
    export var oven = {
        makeCupcake: function( toppings ){
            baker.bake( "cupcake", toppings );
        },
        makeMuffin: function( mSize ){
            baker.bake( "muffin", size );
        }
    }
}

Hivatkozások:

Kódszervezés és modularitás JavaScriptben

Flash lejátszó letöltése

Kódszervezés és modularitás

Vissza a tartalomjegyzékhez

Új Széchenyi terv
A projekt az Európai Unió támogatásával, az Európai Szociális Alap társfinanszirozásával valósul meg.

A tananyag az ELTE - PPKE informatika tananyagfejlesztési projekt (TÁMOP-4.1.2.A/1-11/1-2011-0052) keretében valósult meg.
A tananyag elkészítéséhez az ELTESCORM keretrendszert használtuk.