Třídy, dědičnost a OOP v Javascriptu – III

V předchozích článcích na téma objektově orientovaného programování v Javascriptu jsme probrali způsoby, jak k objektům v JS lze přistupovat a řekli jsme si, jaký způsob je přijatelný a proti kterým lze mít výhrady. Na závěr se podíváme, jak se k problému staví ostatní javascriptové knihovny a jak řešit OOP efektivně.

Seriál: OOP v Javascriptu (3 díly)

  1. Třídy, dědičnost a OOP v Javascriptu – I 15.3.2010
  2. Třídy, dědičnost a OOP v Javascriptu – II 22.3.2010
  3. Třídy, dědičnost a OOP v Javascriptu – III 29.3.2010

Minule jsme si probrali, jak se v Javascriptu vytváří třídy, i jak funguje dědičnost. Dnes si ukážeme další techniky a to, jak se k problému staví různé knihovny.

Každá Javascriptová knihovna (krom jQuery) se snaží nabídnout nějakou stručnější, jednodušší definici třídy a vyjádření dědičnosti, což je v pořádku. BasePrototy­peJS a Mootool­s jdou ještě dál, a snaží se „usnadnit“ volání přepsané metody, což je dle mého skromného soudu zbytečné a omezující. Ukázali jsme si, že volat lze libovolnou metodu, tak funguje Javascript. Přesto si Dean Edwards kdysi dávno řekl, že to nestačí. Jeho řešení spočívá v tom, že každou metodu obalí další metodou, v jejímž closure je schovaná reference na přepsanou metodu, která se před samotným voláním zabalené metody dočasně nastaví na this, pod vlastnost _super. Tuto techniku převzaly výše zmíněné knihovny. Já ji však považuji za zbytečnou, a proto se o ní nebudu dále zmiňovat. Pokud chcete více informací, docela pěkný článek lze nalézt na: http://we­breflection.blog­spot.com/2010/02­/javascript-override-patterns.html 

Nešla by definice třídy zapsat lépe?

Jak je vidět v ukázce z minulého dílu, zápis není zrovna stručný. Je pln provozního kódu. Třídy vytváříme impe­rativně, což není zrovna hezké. Šlo by to deklarativ­něji? Ano, elegantně a snadno. Následující příklad už vypadá lépe.

// třída Person
var Person = $class({

    constructor: function(name) {
        this.name = name;
    },

    getName: function() {
        return this.name;
    }

});

// třída Employee
var Employee = $class({

    Extends: Person,

    constructor: function(name, salary) {
        Person.call(this, name);
        this.salary = salary;
    },

    // přepisujeme metodu z třídy Person
    getName: function() {
        var name = Employee._superClass.getName.call(this);
        return name + ' (zaměstnanec)';
    },

    getSalary: function() {
        return this.salary;
    }

});

Příklad: http://jsfiddle­.net/hJbPr/

Princip tříd a dědičnosti je stejný, jako ve dříve zmíněném kompletním příkladě, ale syntax je stručnější a přehlednější díky funkci $class.

Na scénu přichází $class

$class je tovární funkce a syntax sugar pro deklaraci tříd. Napsal jsem ji pro tento článek, i když implementace vychází z mé vlastní knihovny. Aby předchozí příklad fungoval, může $class vypadat třeba takto:

// pozor, toto je pouze návrh
var $class = function(definition) {
    var constructor = definition.constructor; 
    var parent = definition.Extends;
    if (parent) {
        var F = function() { };
        constructor._superClass = F.prototype = parent.prototype;
        constructor.prototype = new F();
    }
    for (var key in definition) {
        constructor.prototype[key] = definition[key];
    }
    constructor.prototype.constructor = constructor;
    return constructor;
};

Tím bychom měli vyřešený elegantní zápis třídy. Pokud z nějaké třídy dědíme, použijeme vlastnost s názvem Extends. Zatím jsme probrali pouze třídy a dědičnost. Ať zvedne ruku ten, kdo myslí, že dědičnosti je skvělým nástrojem, jak se vyhnout opakovanému kódu. – Tak ti, co si to myslí, si to myslí špatně! Třídy rozhodně nedědíme proto, abychom se vyhnuli opakovanému kódu; to bychom zvládli i bez tříd. Třídy dědíme, protože chceme vyjádřit vztah, jaký mezi sebou mají. Bázová třída reprezentuje nějaký doménový problém. Děděním, tedy vytvářením potomků, problém upřesňujeme.

Zbožštění dědičnosti je častou chybou mnoha programátorů. V jednom svém bývalém zaměstnání jsem viděl projekt, jehož zadání znělo: „Vezmi hodnotu z formulářového políčka, pošli ji na webovou službu, a XML výsledky, co se ti vrátí, vypiš v HTML tabulce.“ Projekt měl dobře přes dvacet tisíc řádků a vyznat se v něm bylo peklo, protože valná většina kódu byly prázdné definice tříd, které ve skutečnosti nic nedělaly, jen existovaly. 

Daleko častější technikou objektově orientovaného programování je agregace. Když začneme psát program, nehledáme co by se kde dalo podědit, ale spíše vytváříme vlastní třídu, která agreguje třídy jiné. Kdy použít dědičnost, a kdy agregaci, vám neprozradím, protože tento článek není o problematice objektového návrhu, ale o jeho implementaci v Javascriptu. Pokud vás problém dědičnost versus agregace zaujal, můžete začít třeba zde: http://s­tackoverflow.com/qu­estions/269496/in­heritance-vs-aggregation

Daniel Steigerwald nabízí školení a konzultace JavaScriptu. Bližší informace zájemci naleznou na daniel.steiger­wald.cz

Mixování

Dynamické jazyky nám nabízejí pokročilejší formu agregace, mixování. Už jsme si ukázali, jak lze třídě přepsat (nebo přidat) metodu.

Person.prototy­pe.serialize = function() {};

Takové úpravy však nejsou pěkné. Kód plný „záplatování a vylepšování“ je jistou poukázkou na pobytovou prohlídku psychiatrické léčebny. Následující příklad už vypadá lépe:

var Serializable = {
    serialize: function() {}
};

// imperativní zápis
Person.mixin(Serializable);

// deklarativní zápis
var Person = $class({
    Mixins: Serializable
});

Mix je samostatným objektem, lze jej tedy snadno sdílet mezi třídami, i testovat. Mixy použijeme vždy, když chceme rozšířit třídu o nějakou funkcionalitu, která není formou její specializace. Mixování se v Javascriptu považuje za náhradu vícená­sobné dědičnosti. Mixy si lze také představit jako rozhraní s implementovanými metodami nebo abstraktní tří­du.

Ve skutečnosti je mix prostý statický objekt, jehož vlastnosti se při mixování nakopírují do vlastnosti prototype. Proč objekt a ne třída? Nechceme, aby bylo možné z mixů vytvářet instance. Už víme, že vztah mezi třídou a instancí je v Javascriptu „živý“ (změníme prototype, a změna se projeví i na všech instancích). Nechceme však ani naznačovat, že něco takového je s mixy možné. Javascript nepodporuje vícenásobnou dědičnost, mix sám o sobě nevstupuje do řetězu prototypové dědičnosti, instanceof operátor mixy ignoruje.

Stejně jako pro dědičnost, ani pro mixy nemá Javascript klíčové slovo, proto si jej implementujeme sami. Ve funkci $class, jsme pro určení bázové třídy použili vlastnost Exten­ds.Vezmeme to jako úzus (PascalCase), a nadefinujeme si další vlastnost: Mi­xins. Aby nám $class moc nenakynula, trochu ji zrefaktorujeme. Implementace $class s podporou mixování vypadá pak takto:

Kompletní příklad

var $class = function(def) {
    // pokud není konstruktor definován, použijeme nový (nechceme použít zděděný)
    var constructor = def.hasOwnProperty('constructor') ? def.constructor : function() { };
    // proces vytváření třídy rozdělíme do kroků
    for (var name in $class.Initializers) {
        $class.Initializers[name].call(constructor, def[name], def);
    }
    return constructor;
};

$class.Initializers = {
    Extends: function(parent) {
        if (parent) {
            var F = function() { };
            this._superClass = F.prototype = parent.prototype;
            this.prototype = new F;
        }
    },

    Mixins: function(mixins, def) {
        // kostruktoru přidáme metodu mixin
        this.mixin = function(mixin) {
            for (var key in mixin) {
                if (key in $class.Initializers) continue;
                this.prototype[key] = mixin[key];
            }
            this.prototype.constructor = this;
        };
        // a přidanou metodu hned využijeme pro rozšíření prototype
        var objects = [def].concat(mixins || []);
        for (var i = 0, l = objects.length; i < l; i++) {
            this.mixin(objects[i]);
        }
    }
};

// mix Serializable

var Serializable = {
    serialize: function() {
        // sem patří nějaká forma serializace, např. JSON.stringify(this);
        return 'serialized';
    }
};

// třída Person
var Person = $class({

    Mixins: Serializable,

    constructor: function(name) {
        this.name = name;
    },

    getName: function() {
        return this.name;
    }

});

// třída Employee
var Employee = $class({

    Extends: Person,

    constructor: function(name, salary) {
        Person.call(this, name);
        this.salary = salary;
    },

    // přepisujeme metodu z třídy Person
    getName: function() {
        var name = Employee._superClass.getName.call(this);
        return name + ' (zaměstnanec)';
    },

    getSalary: function() {
        return this.salary;
    }

});

// Nyní kód otestujeme

// vytvoříme instanci
var joe = new Employee('Joe', 1000);

// všechny tyto testy musí projít
alert([
    joe.serialize() == 'serialized',

    joe.getName() == 'Joe (zaměstnanec)',
    joe.getSalary() == 1000,
    joe instanceof Person,
    joe instanceof Employee,
    typeof Employee == 'function',
    typeof joe == 'object',
    joe.constructor == Employee,
    Employee._superClass == Person.prototype
]); 

Ukázka: http://jsfiddle­.net/me9jZ/

Příklad ukazuje jednoduchý mix Serializable. V praxi se mixování používá pro přidávání složitější funkcionality. Například Yahoo knihovna YUI3, kde se mimochodem používá termín augmentation, používá mixování pro implementaci událostí

Javascript DSL

V průběhu seriálu jsem několikrát použil obrat „chybí klíčové slovo“. Někoho by tak mohlo napadnout, že vlastně Javascript ohýbáme, a že to není pěkné. Situace se hned vyjasní, když si uvědomíme, že vlastně neděláme nic jiného, než vytváříme doménově specifický jazyk. Právě proto, že Javascript je pružný a dynamický jazyk, je snadné vytvořit konstrukty, které se mu do vínku nedostaly, prostě proto, že o nich autor Javascriptu neměl ponětí.

Závěr

Zdaleka jsme nevyčerpali téma, článek by mohl pokračovat dále. V klobouku zůstalo ještě pár zajímavých témat, například: návrhové vzory, AOP, reaktivní extenze, další rozšiřování $class… Ale v nejlepším se prý má přestat. Bude-li zájem, další díly vás rozhodně neminou. Pokud máte doplňující otázky nebo připomínky, použijte diskuzi pod článkem, nebo si přijďte popovídat sedmého dubna na IDF 2010. Budu se těšit.

Nepřehlédněte!

Autor článku Daniel Steigerwald vystoupí s přednáškou na téma Třídy, dědičnost a OOP v Javascriptu na letošní konferenci Internet Developer Forum 2010. Přijďte si jej (a samosebou i další přednášející) poslechnout a zeptat se jich na to, co vás zajímá, ve středu 7. dubna do Národní technické knihovny (registrace nutná).

Independent software gardener, libertarian, web applications consultant and trainer. Google Developer Expert since 2012.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 38

Přehled komentářů

xy dotaz
Daniel Steigerwald Re: dotaz
xy Re: dotaz
Daniel Steigerwald Re: dotaz
Oldis Re: dotaz
xy Re: dotaz
Michal Augustýn Re: dotaz
Oldis Re: dotaz
Michal Augustýn Re: dotaz
Oldis Re: dotaz
Martin Soušek pokračování rozhodně ANO
Ped Re: pokračování rozhodně ANO
Bauglir Re: pokračování rozhodně ANO
Ped Re: pokračování rozhodně ANO
Bauglir Re: pokračování rozhodně ANO
junix Re: pokračování rozhodně ANO
Palo Re: pokračování rozhodně ANO
Aleš Roubíček Re: pokračování rozhodně ANO
Daniel Steigerwald Re: pokračování rozhodně ANO
Aleš Roubíček Re: pokračování rozhodně ANO
Martin Soušek Re: pokračování rozhodně ANO
Daniel Steigerwald Re: pokračování rozhodně ANO
Martin Soušek Re: pokračování rozhodně ANO
aprilchild Re: pokračování rozhodně ANO
martinsousek Re: pokračování rozhodně ANO
Lokutus Re: pokračování rozhodně ANO
xy Re: pokračování rozhodně ANO
xy Re: pokračování rozhodně ANO
keff Re: pokračování rozhodně ANO
Martin Malý Re: pokračování rozhodně ANO
Bauglir Re: pokračování rozhodně ANO
xy Re: pokračování rozhodně ANO
junix Klicove slovo $class
Michal Augustýn Re: Klicove slovo $class
junix Re: Klicove slovo $class
Michal Augustýn Re: Klicove slovo $class
jjjjj chcem pokracovanie
PavelO Úprava kódu pro Node.js
Zdroj: https://www.zdrojak.cz/?p=3193