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. Base, PrototypeJS a Mootools 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://webreflection.blogspot.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 imperativně, což není zrovna hezké. Šlo by to deklarativně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://stackoverflow.com/questions/269496/inheritance-vs-aggregation
Daniel Steigerwald nabízí školení a konzultace JavaScriptu. Bližší informace zájemci naleznou na daniel.steigerwald.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.prototype.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 Extends.Vezmeme to jako úzus (PascalCase), a nadefinujeme si další vlastnost: Mixins. 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á).
Přehled komentářů