Nahoru
 

Návrhový vzor pozorovatel

Návrhové vzory popisují vhodná řešení častých programátorských problémů. Jedním takovým je i problém s notifikacemi a čekáním na události. Někdy potřebujeme reagovat na specifické situace, ale nevíme, kdy nastanou (jestli vůbec). Elegantní řešení pro tento problém nabízí návrhový vzor pozorovatel.

Návrhový vzor pozorovatel (angl. Observer) popisuje dva účastníky: Subjekt (angl. Subject) a Pozorovatel (angl. Observer). Vztah mezi nimi je vcelku jednoduchý – Pozorovatel poprosí subjekt o to, že by chtěl být informován o nějaké události, načež subjekt kdykoliv zaregistruje nebo sám vytvoří danou událost, kontaktuje pozorovatele.

Jako jednoduchý příklad si můžeme představit subjekt jako nějakého správce kláves a pozorovatele jako program, který chce přehrát zvuk na stisk klávesy „X“. Náš program (pozorovatel) kontaktuje správce klávesnice, že by chtěl být notifikován, kdykoliv uživatel stiskne klávesu „X“ a dále už nic nedělá. Správce klávesnice nějakým způsobem čte všechny připojené klávesnice a skenuje I/O. Až uživatel konečně stiskne klávesu „X“, správce klávesnice zpracuje nová data a zároveň kontaktuje našeho pozorovatele, který přehraje zvuk.

V praxi je pozorovatel často realizován skrze objekt (nebo klíčové slovo) Událost (angl. Event). Subjekt ve svém rozhraní tyto událostní objekty poskytne jako veřejné. Události mohou být například: Stisk klávesy „X“, stisk jakékoliv jiné klávesy, změna velikosti prohlížečového okna, klik myší apod. Noví pozorovatelé kontaktují tuto událost a zažádají, aby kdykoliv subjekt tuto událost „aktivuje“, se zavolala metoda, kterou pozorovatel události předá. Událost si pamatuje seznam pozorovatelů a jejich poskytnutých metod a jakmile subjekt vyvolá danou událost, proiterují se všichni pozorovatelé a zavolají se poskytnuté metody. Zdá se to složité, nicméně jednoduchý návrh události popisuje příklad níže.

Event – schéma
Obrázek č. 1: Princip subjektu, události a pozorovatele

Návrhový vzor pozorovatel jasně poukazuje hned na řadu výhod:

  • Docílíme efektivity tím, že jako pozorovatel nemusíme aktivně čekat nebo blokovat vlákna.
  • Docílíme spojení dvou různých kódu bez toho, aniž bychom mezi nimi vytvořili oboustrannou závislost – jsou volně vázané.
  • Je jedno, kdo jsou pozorovatelé a kolik jich je. Subjekt nemusí být modifikován, pokud chceme další funkce s klávesami.

Příklad

Návrhový vzor pozorovatel se může zdát na první pohled jako složitý, nicméně pojďme si ukázat na příkladu, že to není až tak kompilované. Celý kód i s testy naleznete v repositáři na GitHub.

Mějme objekt SimpleEvent, který se chová jako událost – tedy prostředník mezi subjektem a pozorovatelem. Abychom mohli událost nazvat událostí, musí mít minimálně 2 metody – na přidání nového pozorovatele a na notifikaci všech pozorovatelů.

public class SimpleEvent
{
    private List<VoidFunction> observerFunctions = new List<VoidFunction>();

    /// <summary>
    /// Metoda, která přidá nového pozorovatele (resp. jeho funkci na pozdější zavolání)
    /// </summary>
    /// <param name="newObserverFunction"></param>
    public void Add(VoidFunction newObserverFunction)
    {
        observerFunctions.Add(newObserverFunction);
    }

    /// <summary>
    /// Metoda, která je volána subjektem a která notifikuje všechny pozorovatele
    /// </summary>
    public void Invoke()
    {
        foreach (var observerFunction in observerFunctions)
            observerFunction();
    }
}

public delegate void VoidFunction();

Tento jednoduchý event nyní můžeme poskytnout jako veřejnou vlastnost objektu pro sken kláves. Jestliže skener zachytí nějaký stisk, ihned pomocí událostního objektu notifikuje všechny pozorovatele.

/// <summary>
/// Třída skenující vstupy na klávesnici (pouze simulátor)
/// Tato třída používá námi vytvořený jednoduchý event
/// </summary>
class KeyboardScanner_SimpleEventClass
{
    public SimpleEvent OnKeyPress { get; } = new SimpleEvent();

    public void PressKey(string key)
    {
        //--Třída si stisk zpracuje--

        //Notifikace pozorovatelů
        OnKeyPress.Invoke();
    }
}

A nyní se již kdokoliv může zaregistrovat k události.

static void Demo_KeyboardScanner_SimpleEventClass()
{
    //Subjekt
    var keyboardScanner = new KeyboardScanner_SimpleEventClass();

    //Vytvoření pozorovatele (resp. my jsme pozorovatel a přidáme si funkci na zavolání/nofitikaci)
    keyboardScanner.OnKeyPress.Add(() =>
    {
        Console.WriteLine("Subjekt mě notifikoval!");
    });

    //Stiskneme klávesu a subjekt nás automaticky notifikuje
    keyboardScanner.PressKey("space");
}

Nyní však máme problém, že nevíme, která klávesa byla stisknutá. To můžeme opravit ještě tak, že bychom událost vytvořili parametrizovatelnou. Událost by poté mohla vypadat takto:

public class ParameterizableEvent<Subject, Args>
{
    private List<ParameterizableEventHandler<Subject, Args>> observerFunctions 
        = new List<ParameterizableEventHandler<Subject, Args>>();

    /// <summary>
    /// Metoda, která přidá nového pozorovatele (resp. jeho funkci)
    /// </summary>
    /// <param name="newObserverFunction"></param>
    public void Add(ParameterizableEventHandler<Subject, Args> newObserverFunction)
    {
        observerFunctions.Add(newObserverFunction);
    }

    /// <summary>
    /// Metoda, která je volána subjektem a která notifikuje všechny pozorovatele
    /// </summary>
    /// <param name="caller">Reference na subjekta, který invoke zavolal</param>
    /// <param name="arguments">Předání stavových argumentů, například jaká klávesa byla stisknuta apod.</param>
    public void Invoke(Subject caller, Args arguments)
    {
        foreach (var observerFunction in observerFunctions)
            observerFunction(caller, arguments);
    }    
}

public delegate void ParameterizableEventHandler<Subject, Args>(Subject caller, Args arguments);

Třída využívající událost pak musí definovat typy argumentů a při každém zavolání pozorovatelů musí poskytnout parametry.

/// <summary>
/// Třída skenující vstupy na klávesnici (pouze simulátor)
/// Tato třída používá námi vytvořený parametrizovatelný event
/// </summary>
class KeyboardScanner_ParameterizableEventClass
{
    public ParameterizableEvent<KeyboardScanner_ParameterizableEventClass, KeyboardScanner_EventArguments> OnKeyPress { get; } 
            = new ParameterizableEvent<KeyboardScanner_ParameterizableEventClass, KeyboardScanner_EventArguments>();

    public void PressKey(string key)
    {
        //--Třída si stisk zpracuje--

        //Notifikace pozorovatelů
        OnKeyPress.Invoke(this, new KeyboardScanner_EventArguments(key));
    }
}

/// <summary>
/// Třída nesoucí informace o události
/// </summary>
class KeyboardScanner_EventArguments
{
    public string PressedKey { get; set; }

    public KeyboardScanner_EventArguments(string pressedKey)
    {
        PressedKey = pressedKey;
    }
}

Jako pozorovatel to máme již snadné. Stačí do „naslouchací“ funkce dodat parametry.

static void Demo_KeyboardScanner_SimpleEventClassArguments()
{
    //Subjekt
    var keyboardScanner = new KeyboardScanner_ParameterizableEventClass();

    //Vytvoření pozorovatele (resp. my jsme pozorovatel a přidáme si funkci na zavolání/nofitikaci)
    //Dozvíme se dokonce i nějaké informace
    keyboardScanner.OnKeyPress.Add((caller, args) =>
    {
        Console.WriteLine("Subjekt mě notifikoval!");
        Console.WriteLine("Stiskla se klávesa: " + args.PressedKey);
    });
   
    //Stiskneme klávesu a subjekt nás automaticky notifikuje
    keyboardScanner.PressKey("space");
}

Některé jazyky jako například výše použitý C# disponují nativními nástroji pro tyto události. Jak jej řeší C# si můžete počíst v C# dokumentaci anebo shlédnout příklad v repositáři pro tento projekt.

Callback

Jako další ještě jednouší příklad můžeme uvést callback, který je populární hlavně v JavaScriptu. Jedná se o jednorázové zavolání metody (většinou) při asynchronních operacích, jako například vyslání http požadavku (například pro ajax). Callback je však nyní nahrazen konstruktem Promises, nicméně ten stejně v jádře funguje na bázi callbacků.

Callback je ve svém podstatě nejjednodušší forma návrhového vzoru pozorovatele, ale zdaleka tento vzor nereprezentuje v plné míře jeho potenciálu ani výhod. Callback má oproti řádnému využití událostí spoustu nevýhod (například Callback hell).

let xhr = new XMLHttpRequest();
xhr.open('GET', '/article/xmlhttprequest/example/load');

//Na načtení proběhne tato funkce, což nevíme, kdy bude – jedná se o callback
xhr.onload = function() {
  //Zpracovat výsledek
};

xhr.send();

Návrhový vzor pozorovatel patří mezi ty nejdůležitější návrhové vzory, což dokazuje i nativní podpora některých jazyků. Jedná se o elegantní způsob, jak volně propojit třídy a zároveň se neobtěžovat s aktivním čekáním nebo blokováním vláken. Události by měly být v arzenálu každého programátora.