Synaptiko blog

Machina.js – Konečný stavový automat v JavaScriptu

Překlad článku: machina.js – Finite State Machines in JavaScript

Nedávno jsem pracoval na malém frameworku, který měl za cíl poskytnout flexibilitu při psaní javascriptu za pomoci konečného automatu. Vypůjčil jsem si několik myšlenek ze světa Erlangu a jeho OTP (chování gen_fsm je úžasné!). A výsledkem mého snažení je machina.js.

Zaprvé – Co je to konečný automat?

Konečný automat (zkráceně KA) je teoretický výpočetní model, který se v daném čase nachází pouze v jednom z konečné množiny stavů. To znamená, že může odpovídat různě na stejné vstupy a to v závislosti na aktuálním stavu. Jednoduchým příkladem je semafor. Doprava je řízena odlišně v závislosti na barvě světla semaforu. Zřejmě říká, že doprava se má zastavit, pokud je aktuálním stavem červené světlo a že doprava má pokračovat, pokud svítí zelená. To je mocné schéma – schopné modelovat a řešit množství problémů na libovolné úrovni aplikace – a webové aplikace na straně klienta nevyjímaje. Zvažme několik možných scénářů, které nám může KA pomoci vyřešit:

  • Inicializace aplikace: Můžete mít řadu stavů, ve kterých je vaše aplikace spuštěná a uživatel už s ní může pracovat. Během této doby můžete přednačítat externí šablony, přijímat data, dynamicky renderovat DOM elementy atd.
  • Offline vs Online: Pokud vaše webová aplikace potřebuje podporovat práci offline, tak můžete KA využít k abstrakci jak (a kdy) budou data ukládána na základě právě těchto dvou stavů.
  • „Hromadné“ změny UI: Je docela možné, že UI vaší webové aplikace změní konfiguraci (např. se mezi sebou vymění několik view) na základě toho, jak se v ní uživatel pohybuje. KA může abstrahovat tyto „UI konfigurace“ (neboli stavy), a umožní vám jednoduše říct (pomocí pseudokódu) „přechod do stavu: ‘Zákaznický modul’“, kde se KA může hladce vypořádat s předchodem z aktuálního UI (což může zahrnovat skrytí nějakých prvků, odebrání posluchačů událostí, animaci přechodu atd.) do nového UI (což může zahrnovat zobrazení nových prvků, načtení šablon a dat, animace atd.)
  • Životní cyklus view: Pokud používáte framework jako je Backbone.js (nebo podobný), který má koncept tzv. view objektů, tak vám KA může pomoci zaobalit/spravovat životní cykly view zaměřené/odpovídající odlišně na zprávy/události na základě jednoho z mnoha možný stavů (jako např. „neinicializováno“„nevyrenderováno“, „zobrazeno“, „skryto“ atd.)

Je určitě možné se s výše uvedenými problémy vypořádat použitím tzv. deferred. Ale poté, co jsem strávil nějaký čas v kódu doslova prošpikovaném samými deferred (mnohokrát a na různých úrovních navzájem do sebe vnořených) jsem přišel na to, že se velice rychle staly obtížně testovatelnými, debugovatelnými a hlavně se semkly do řady kroků, po kterých jsem chtěl, aby běžely odděleně. Zvažte například scénář „Inicializace aplikace“ popsaný výše. Pokud část inicializačního procesu přijímá data, mohl bych chtít umožnit uživateli “refreshnout“ tyto data bez znovu vyvolání celého inicializačního procesu. Zatímco tohoto chování mohu lehce dosáhnout, pokud použiju druhý deferred (nebo jejich soubor), tak pokud místo toho abstrahuji problém pomocí KA, mohu přejít zpět do stavu, kde aplikace data načítala a začít je načítat znovu. A já miluju, když se v míru snoubí SOLID a DRY dohromady (když dohromady vytvoří deodorant, který osvěží zatuchlý kód).

Vliv Erlang/OTP na machina.js

Měl jsem veliké štěstí, že jsem mohl strávit několik posledních let podstatné množství času učením se o úžasně osvěžujícím světě Erlangu. Knihovna OTP pro Erlang poskytuje několik „chování“ (něco jako ’šablona’ pro vytváření procesů, které se řídí určitými API konvencemi) – a jedním z nich je i Gen_Fsm chování. Jsou zde dvě věci, které dle mého, činí chování KA v Erlangu tak supermocné: zaprvé - aktuální stav je jméno funkce, která bude spuštěna a za druhé – „pattern matching“ v Erlangu činí způsob, jak se stav vypořádává se zprávami/vstupy neuvěřitelně stručným, flexibilním a snadno rozšiřitelným. Pojďme se podívat na krátkou ukázku:

Pokud máme KA v Erlangu, který má stavy „initializing“, „loading“„loaded“, tak můžeme mít něco jako následující kus kódu:

initializing({load_customer, CustomerId}, State) ->
    NewState = State#state{ customer_id = CustomerId },
    {next_state, loading, NewState}.

loading({succeeded, Customer}, #state{customer_id = CustomerId} = State) ->
    event_module:notify("Successfully Loaded Customer ~p ~n", [ CustomerId ]),
    NewState = State#state{ customer = Customer },
    {next_state, loaded, NewState}.

loaded({add_order, Order}, #state{customer = Customer}) ->
    order_module:process_order(Customer, Order).

Předchozí kód nám ukazuje tři funkce v jazyce Erlang. Každá z nich reprezentuje jeden z možných způsobů, jak zacházet s příchozí zprávou v daném stavu. Funkce „initializing“ bude spuštěna pouze v případě, že aktuální stav KA je „initializing“ a pokud je první z argumentů zprávy atomem typu „load_customer“. Dokonce by zde mohla být řada dalších funkcí „initializing“, které by se přetěžovaly a každá by zpracovávala odlišné typy argumentů – a to je jen jedna malá ukázka z krásného světa „pattern matching“ jazyka Erlang. Všimněte si, že na konci „initializing“ funkce se vrací seznam, který říká KA, že má přejít do dalšího stavu „loading“ a zároveň mu předává objekt/záznam s „NewState“.

Funkce „loading“ by byla spuštěna pouze v případě, že je KA ve stavu „loading“ a že prvním argumentem zprávy je atom typu „succeeded“. Tady si již můžete povšimnout, že celý koncept je založený na tom, že aktuální stav je zároveň jménem funkce, která bude spuštena, aby zpracovala zprávu či událost a zároveň schopnost „zapadnout“ do správné funkce na základě hodnot argumentů, které jsou funkci předány. A to jsou přesně ty dvě klíčové věci, které jsem si z Erlang/OTP vypůjčil a pokusil se je napsat do pěkného JavaScriptového FSM frameworku.

A zde už je přepis výše uvedených funkcí z Erlangu do mého machina.js frameworku:

var fsm = new machina.Fsm({
    states: {
        initializing: {
            // další obslužné funkce pro stav inicializace by přišly sem
            load_customer: function(customerId) {
                this.customer_id = customerId;
                this.transition("loading");
            }
        },
        loading: {
            // další obslužné funkce pro stav načítání by přišly sem
            succeeded: function(customer) {
                this.channel.publish("Successfully Loaded Customer " + customer.id);
                this.customer = customer;
                this.transition("loaded");
            }
        },
        loaded: {
            // další obslužné funkce pro stav načteno by přišly sem
            add_order: function(order) {
                orderProcessor.processOrder(this.customer, order);
            }
        }
    }
});

Takže, jak jsem to udělal?

Machina.js organizuje stavy KA do objektů kde každému stavu odpovídá objektový literál, který obsahuje funkce, které jsou pojmenovány tak, aby reprezentovaly typ zprávy nebo události, který ošetřují nebo zpracovávají. Tento stavový objekt – ačkoliv zřejmě není funkcí – je po vzoru KA v Erlang/OTP pouze způsobem, jak spustit funkce uvnitř tohoto stavu. Jména funkcí uvnitř stavového objektu pak odpovídají tomu, k čemu v Erlangu slouží „pattern matching“. Tedy k nalezení správné funkce, která může být spuštěna aby zpracovala příchozí zprávu a data.

Například, řekněme, že máme KA založený na machina.js, který je ve stavu „ready“. Pokud přijme zprávu, která mu říká, aby ošetřil „customer.getData“ nebo pokud někdo přímo spustí „handle (‘customer.getData’, /* argumenty */)“, pak se KA podívá po „this.states.ready“ (jinak řečeno: mám nějaký handler pro stav s názvem „ready“?) a potom zkusí ‘this.states.ready["custome.getData"]‘. Pokud je nalezena nějaká funkce, která umí tento vstup ošetřit, tak je spuštěna a jsou jí předány argumenty poskytnuté ve volání funkce „handle“ (tedy něco takového ‘this.states.ready ["customer.getData"](/* argumenty */)’). KA v machina.js může mít také tzv. funkci „catch-all“ (indikovaný jménem „*“), která se vypořádá s jakoukoliv zprávu, pro kterou není nalezena žádná odpovídající funkce. Pokud KA nemůže najít odpovídající funkci nebo funkci „catch-all“, pak je zpráva jednoduše ignorována.

Za hranicí základů

Hodně jsem se v mých nápadech pro machina.js inspiroval od Alexe Robsona (který je mnohem lepší vývojář v Erlangu než jsem já a krmí mě výživnými nápady i v ostatních projektech). Alex mně inspiroval například s funkcemi „deferUntilTransition“ a „deferUntilNextHandler“. Volání „deferUntilTransition()“ uvnitř funkce uvnitř stavu uloží aktuální argumenty do fronty pro zpracování po přechodu do stavu nového. A v tom je velká síla, protože vám to umožňuje se tolik nestarat o příchozí zprávy, které neumíte zpracovat. Můžete je odložit na později a zpracovat je až po přechodu do stavu, který tyto zprávy zpracovat umí. Volání „deferUntilTransition“ má volitelný argument „stateName“, který způsobí, že se se zprávou čeká dokud KA nepřejde do specifikovaného stavu. Volání „deferUntilNextHandler“ je podobné, až na výjimku, že čeká pouze až se vykoná aktuální zpracování a zprávu zkusí zpracovat poté znovu. Tedy nečeká na přechod do dalšího stavu.

Další věc, kterou jsem pro machina.js zamýšlel bylo začít s integrací tzv. „message bus“ [sběrnice]. Machina podporuje dvě jiné JavaScriptové knihovny, které „message bus“ poskytují: postal.js a amplify.js. Stojí za to poznamenat, že zatímco postal.js podporuje exchanges a wildcard bingings a proto vám dává trochu víc možností, když se integrujete s machina.js, tak obě knihovny vám umožňují jednoduše komunikovat s FSM pomocí messagingu (Kurňa dobré oddělení logiky, ne?). Samozřejmě se po vás nevyžadje používat messaging pro komunikaci s machina.js a můžete si klidně používat API přímo jak pro spouštění zpracování ve stavech, tak pro přihlašování se k událostem (pomocí funkcí „on“ a „off“). Také, pokud už máte nějakou jinou oblíbenou pub/sub knihovnu, si pro ní můžete napsat svůj provider a integrovat ji tak s machina.js - inspiraci hledejte právě v postal.js a amplify.js.

Navíc „machina.utils“ poskytují další možnosti rozšíření, které vám umožňují měnit defaultní konfigurace ze které jsou KA vytvářeny a měnit způsob, jak nakládat s příchozími zprávami, pokud používáte machina.js ve spojení s poskytovatelem „message bus“.

Zajímá mě váš názor

Byl jsem velice překvapen, když jsem zaslechl ostatní JavaScriptové vývojáře diskutovat o použití KA. To jen posílilo mé přesvědčení, že KA je často kvalitnější abstrakcí pro řešení určitých problémů než deferred. Pokud se vám zdál můj text zajímavý, tak si machina.js zkuste sami. V repozitáři jsou dvě ukázky, takže stahujte, zkoumejte a užijte si to! A pak se vraťte a sdělte mi váš názor :-)

Pět dohod

  1. Miřte slovem přesně (Nehřešte slovem)
  2. Neberte si nic osobně
  3. Nevytvářejte si žádné domněnky
  4. Vždy dělejte vše, jak nejlépe dovedete
  5. Buďte skeptičtí, ale naslouchejte

Praxe dělá mistra :-)

Více:

Projekt Synaptiko.SimplePaging na GitHubu

Na GitHub jsem pushnul svůj první testovací projekt Synaptiko.SimplePaging, na kterém se chci naučit objektové programování v javascriptu podle „vzorů“ popsaných zde:

Projekt rozhodně není ve finální verzi a čeká ho ještě spousta refaktoringu a hlavně přidávání dalších featur. Inspiraci pro refactoring hledám také v knize Čistý kód.

Co je zatím implementováno? Pouze základní funkce synaptiko.pagination.create a dodatečné constructor-funkce Page a PageRange. Pokusil jsem se přidat i nástřel vizuálních a jednotkových testů. Více se dočtete v README.

Založení GitHubu

Protože se chci vrhnout do vod OpenSource, rozhodl jsem si založit GitHub pro sdílení kódů a projektů: https://github.com/synaptiko

Vše budu vydávat „pod hlavičkou“ Synaptiko (nebo Synaptiko.cz, protože synaptiko.com je existující stránka)

Čas nás nezastaví

Je čas vystavit se času napospas,
aby z nás tvrdými životními lekcemi
spásl touhu být v tomto životě spasen.

Nic nás nespasí.
Nepřijde mesiáš.
Nepřijde vykoupení.
Nepřijde úleva z této nesnesitelné lehkosti bytí.

Je to k sblití, ale není zbytí…
Raději neuchylovat se k pití
či myšlenkám o nebytí.

Poddej se bytí
a to tě chytí!!!

Tímto chci vymítit
tu prokletou otázku
o bytí či nebytí
z mé pošahané hlavy.

Méně

Přestaň kupovat nepotřebné krámy.
Zbav se poloviny věcí. Nauč se být spokojený s tím, co máš.
Pak se znovu zbav poloviny ze zbytku.

Udělej si seznam čtyř nejdůležitějších věcí tvého života,
přestaň dělat nepodstatné věci.
Každý den dělej napřed tyto nejdůležitější věci,
vyvaruj se rozptylování,
zaměř se na přítomný okamžik.

Opusť připoutanost dělat a mít více.
Zamiluj se do mála.

Volný překlad z mnmlist.com/less