Sada pro modeláře

18. 8. 2014 19:48 (aktualizováno) Petr Blahoš

Když jsem tak před deseti lety viděl Java Swing, bylo to jako zjevení. V porovnání s tím, jak se bastlilo uživatelské rozhraní jinde (GTK, YAST, MFC), tohle mělo hlavu a patu. Pro neznalé: Figurují v tom modely a listenery. Ovládací prvek (Komponenta UI, Widget, Control, ať už tomu říkáme jak chceme) má model. Obsah prvků neměníme na tom prvku, ale úpravou obsahu modelu. Když se v modelu něco stane, tak model „odpálí“ (fire) událost, a někdo (třeba ten ovládací prvek) poslouchá (implementuje listener), a tím se o změně modelu dozvídá. Naopak, ten ovládací prvek, když se na něm něco stane, tak taky „odpálí“ tu událost, a kdo poslouchá, (implementuje a pověsí listener), ten se to dozví. Pro pořádek, říká se tomu MVC.

Potíž se Swingem je v tom, že to je celé takové hrozně ukecané a těžkopádné. Nebylo by pěkné, kdybychom měli obyčejnou datovou strukturu (objekt, list, mapu, …), která by nám „odpalovala“ tyhle události, ale pracovalo by se s ní tak, jak jsme zvyklí? Neboli, neměli bychom model postavený podle toho, jak to chce UI knihovna, ale postavili bychom si model tak, jak jej chceme my, a s pokud možno co nejmenším úsilím by nám generoval události pro UI nebo cokoliv jiného? Tak se na to podíváme.

Zkusíme si udělat takový list, ale nejprve si musíme něco ujasnit. V Pythonu se blbě dělají neměnné (immutable) objekty. Přidání nebo smazání v listu se detekuje snadno, ale modifikace objektu v listu už prakticky nejde. Nebo „zmrazení“ objektu vloženého do listu? Něco na ten způsob sice existuje, ale má to svoje problémy. Proto bude lepší, když si povolíme mít jako prvky toho listu pouze neměnné objekty, což jsou co já vím primitivní typy (int, long, float, str, unicode), frozenset, a tuple, jehož prvky jsou zase jen neměnné typy. Kdybychom povolili něco jiného, tak bychom nedokázali detekovat změny. Např. tohle přiřazení: data[15] = [1, 2, 3,] ješte poznáme tak, že v data definujeme vlastní __setitem__. Jenže nedokážeme detekovat např. data[15].append(4). Proto věci v našem modelu musí být immutable, a my si ten model musíme postavit tak, aby neumožnil nic, co nechceme.

Jestliže děláme z toho listu tabulku, tak se nám bude hodit ten tuple, jestli to bude seznam, tak nám stačí text nebo číslo. Zjednodušeně:

class HotBase(object):
    def __init__(self):
        self.listeners = []

    def add_listener(self, listener):
        self.listeners.append(listener)

    def _fire(self, name, key):
        for listener in self.listeners:
            listener(self, name, key)


class HotList(HotBase):
    def __init__(self):
        super(HotList, self).__init__()
        self.data = []
        self._fire("reset", None)

    def __delitem__(self, key):
        del self.data[key]
        if type(key) is slice:
            self._fire("reset", key)
        else:
            self._fire("delete", key)

    def __setitem__(self, key, value):
        if type(key) is slice:
            for i in value:
                self._validate_value(i)
            self.data[key] = value
            self._fire("reset", key)
        else:
            self.data[key] = self._validate_value(value)
            self._fire("update", key)

    def insert(self, key, value):
        self.data.insert(key, self._validate_value(value))
        self._fire("insert", key)

    def _validate_value(self, val):
        if type(val) in (int, long, float, str, unicode, ):
            return val
        if isinstance(val, tuple) or isinstance(val, frozenset):
            for i in val:
                self._validate_value(i)
            return val
        raise TypeError("Not immutable.")
Vynechávám __len__, __getitem__, append. Důležité je _validate_value, která rekurzivně ověří, že hodnota je primitivní typ nebo tuple primitivních typů. Tím pádem nemůžeme udělat hot_value[14] = [ 1, 2, 3, ]. Další zajímavost spočívá v tom, že s tím, co dáme do hranatých závorek se dají dělat v pythonu psí kusy. Můžeme např. napsat hot_value[10:20:2] = (2, 4, 6, 8, 10,). Tohle přiřadí do pole na pozice 10, 12, 14, 16, 18. Nevím, jestli se to dá smysluplně využít, ale musíme s tím počítat, takže když máme v __setitem__ a __delitem__ jako parametr slice, tak rovnou generujeme událost reset. Kromě toho máme události update, delete a insert.

Teď, jak ty události budeme chytat? Ve Swingu je každý listener objekt implementující konkrétní interface, takže je tam spousta metod s dlouhými jmény. V kontrastu k tomu jsem prozatím zvolil něco strašně jednoduchého: zavolá se listener, vždy se třemi stejnými parametry:

  • objekt, ve kterém událost vznikla (z pohledu „střelce“ je to self)
  • jméno události
  • klíč – identifikace položky v objektu

takže nerozlišujeme různé druhy listenerů, model vlastně posílá všechno všem, a příjemce se musí postarat o nasměrování události kam patří. Nevím, jestli u toho zůstanu, zdá se mi, že ti, co vybrali jiný model byli zkušenější než já, a asi pro to měli důvod. A takový listener pak může vypadat třeba takto:

    def on_model(self, event_source, event_name, data):
        """
            An event has been "fired" in the model. It is our responsibility
            to handle only the events we care about.
        """
        handler = "handle_%s" % event_name

        if handler in dir(self):
            getattr(self, handler)(event_source, event_name, data)
        else:
            logging.warn("Unhandled event: %s", event_name)

    def handle_reset(self, event_source, event_name, data):
        self.DeleteAllItems()
        for (i, data) in enumerate(self.event_source):
            self.add_item(i, data)

    def handle_insert(self, event_source, event_name, data):
        self.add_item(data, event_source[data])

    def handle_delete(self, event_source, event_name, data):
        self.DeleteItem(data)
Chcete-li si to zkusit, najdete v github/modellerkit step01 jednoduchý příklad se tabulkou (budete potřebovat wxPython). Je to zatím takové jednosměrné – od modelu k view. Příště udělám model pro vybranou položku v tabulce, a asi se podíváme na složitější případ.

Sdílet