Mixins

Od Petr Zemek, 2008-12-23

Na jednom týmovém sezení (při řešení projektu do předmětu Počítačová grafika) jsem nadhodil techniku známou jako Mixins. Bohužel jsem ji tehdy nedokázal dostatečně vysvětlit a její použití nebylo v dané situaci vhodné, takže bych to chtěl tímto příspěvkem napravit a ukázat, že tato technika je v praxi velice užitečná a že využití mixinů by nemělo být obecně považováno za "bad practice" či za anti-vzor.

Co to ty mixiny vlastně jsou a kde je použít?

Za mixin je (v objektově orientovaných programovacích jazycích) považována třída, která přidává určitou funkcionalitu jiným třídám, které z ní dědí a není určena k tomu, aby byla instanciována sama o sobě. Obecně není nutné, aby se jednalo o třídu a aby se z ní dědilo (viz možné implementace dále), ale tento způsob implementace je nejčastější.

Nejlépe to bude vidět na následujícím příkladu. V pythonu existuje modul SocketServer obsahující třídy UDPServer a TCPServer. Standardně jsou všechna spojení obsluhována v jednom procesu. Dále jsou zde dva mixiny: ForkingMixIn a ThreadingMixIn. Pokud budeme chtít, aby náš server zpracovával každé spojení v novém vlákně, tak jednoduše vyvtoříme novou třídu ThreadingTCPServer, která se takto chová:

class ThreadingTCPServer(ThreadingMixIn, TCPServer):
        pass

Podobně, pokud bychom chtěli, aby se každé spojení zpracovávalo v samostatném procesu, tak bychom dědili z ForkingMixIn. Využitím mixinů lze dosáhnout elegantních řešení, kdy není třeba modifikovat původní třídu za účelem přidání funkcionality. Dále je vhodné podtknout, že u žádné z těchto mixin tříd nedává smysl, aby byly použity samy o sobě.

Jako další příklad lze uvést situaci, kdy chceme k nějaké třídě přidat časové razítko (nebo jinou vlastnost jako je např. sériové číslo). V C++ to lze udělat takto:

class TimeStamped {
        long timeStamp;
    public:
        TimeStamped(): timeStamp(time(0)) { }
        long getStamp() { return timeStamp; }
};

Pokud chceme, aby všechny objekty určité třídy měly v sobě informaci, kdy byly vytvořeny, pak (stejně jako v minulém příkladu) využijeme vícenásobnou dědičnost:

class MyClass: public SomeParentClass, public TimeStamped {
       // Class definition
       // ...
};

Jak je vidět, funkcionalitu lze takto přidat i do tříd, které mají předka a u kterých se nepočítalo s tím, že by u svých instancí měly uchovávat určitou informaci.

Jako poslední příklad jsem zvolil situaci, kdy v C++ chceme, aby daná třída nešla kopírovat a nechceme u každé takovéto třídy definovat kopírovací konstruktor a operátor přiřazení jako privátní a význam tohoto konání zdokumentovat (tradiční přístup, který je ale zdlouhavý a při kterém se snadno zapomene na některý z operátorů). Pak lze využít tuto třídu:

class NonCopyable {
    protected:
        NonCopyable() {}
        ~NonCopyable() {}
    private:
        NonCopyable(const NonCopyable &);
        NonCopyable & operator=(const NonCopyable &);
};

A výslednou třídu, která nemá být kopírovatelná, vytvořit takto:

class CantCopy: private NonCopyable { };

Vlastnosti mixinů

U mixinů si lze všimnout následujících vlastností:

  • V implementaci se sice využívá dědičnost, ale tato dědičnost neznamená generalizaci/specializaci, nýbrž se jedná pouze o přidání funkcionality. Na mixiny lze tudíž pohlížet jako na rozhraní implementující své metody.
  • Mixiny lze využít i ve statických jazycích, nicméně největší síly dosahují v dynamických jazycích, které umožňují měnit třídy za běhu programu, takže je možné (za běhu) přidat či odebrat třídě funkcionalitu. Výhodu (při implementaci) mají také jazyky, které umoňují nějakou formu vícenásobné dědičnosti, kde pak lze u jedné třídy (která může již být specializací jiné třídy) využít funkcionality více mixinů.
  • Bruce Eckel vidí koncept mixinů jako velice podobný návrhovému vzoru dekorátor, ale přidání funkcionality se děje pomocí dědění, nikoliv kompozice a považuje koncept mixinu jako více přirozený:

    Because the resulting class contains all the methods of interest just like any other class, the mixin approach is arguably more natural than a decorator, because the type of the object that results from using decorators is the last type that it was decorated with, whereas the type of the mixin is all the types that have been mixed together.

Implementace v některých programovacích jazycích

Na tomto místě bych chtěl stručně popsat, jak se dá tato technika implementovat v různých programovacích jazycích. Vycházím z nalezených článků a příspěvků, takže pokud u některého z jazyků znáte jiný/jednodušší způsob, určitě jej zmiňte v komentáři.

C++

Při implementaci mixinů v C++ lze využít vícenásobnou dědičnost a parametrizované typy (viz druhý a třetí příklad v mém příspěvku).

Další informace: 1, 2 a 3.

Python

Jelikož je python dynamický jazyk a umožňuje modifikovat třídy za běhu, lze buď použít mixin při vytváření třídy (viz první příklad) nebo lze definovat následující funkci, která umožňuje "při-mixování" (lze vytvořit i podobnou funkci pro "od-mixování"):

def MixIn(pyClass, mixInClass):
    if mixInClass not in pyClass.__bases__
        pyClass.__bases__ += (mixInClass,)

Další informace: 1 a 2.

Ruby

Ruby podporuje pouze jednoduchou dědičnost, nicméně tento "nedostatek" lze nahradit využitím modulů, kdy se vytvoří modul implementující funkcionalitu mixinu a jeho vložením (include/extend) do třídy dojde k přidání funkcionality do této třídy.

Další informace: 1.

C#

C# také nepodporuje vícenásobnou dědičnost, takže mixiny se v něm implementují pomocí tzv. extension methods (nepřekládám), kdy vytvoříme jednoduchý interface a poté definujeme (mimo tento interface) ony extension methods, které berou jako parametr dané rozhraní a implementují určitou funkčnost. Tento přístup má tu výhodu, že při přidání nové metody není třeba překompilovat třídu, která implementuje dané rozhraní (mixin).

Další informace: 1.

Java

Java nepodporuje vícenásobnou dědičnost ani podobnou vlastnost jako C#, nicméně mixiny se zde (poněkud krkolomně) dají řešit přes vnitřní třídy (inner classes), ovšem tento přístup mě přijde těžkopádný a neflexibilní. Ani Java 7 tuto situaci zřejmě nezlepší...

Další informace: 1.

PHP

U PHP má cenu hovořit o objektové orientaci až od verze 5 (ve verzi 4 už sice "jakýsi" objektový základ byl, ale...). PHP také podporuje pouze jednoduchou dědičnost a nic, co by mixiny přímo umožňovalo, takže je třeba určité okliky. Jedním ze způsobů je si nadefinovat třídu, která bude implementovat "magickou" metodu __call() (pokud instanci této třídy někdo pošle zprávu, které tento objekt nerozumí, tak se tato metoda zavolá) a bude předkem tříd, do kterých má být možné přidávat funkčnost přes mixiny. U tříd, které budou z této třídy dědit se vytvoří privátní pole s názvy tříd, které do dané třídy přidávají funkčnost a v implementaci metody __call() se zajistí zavolání správné metody u příslušného mixinu.

Toto řešení určitě není ideální, ale je to jedna z možností v případě, že chceme mixiny využít v PHP.

Další informace: 1.

Smalltalk

Ani Smalltalk nepodporuje vícenásobnou dědičnost. Je zde možno postupovat podobně jako v případě PHP, kdy se upraví metoda doesNotUnderstand třídy Object (z této třídy všechny ostatní třídy dědí) tak, že pokud dané zprávě objekt nerozumí, vyzkouší se všechny mixiny (uloženy v kolekci), zda některý z nich zprávě rozumí.

Další informace: 1.

Závěr a negativa

Samozřejmě, každá technika má své mouchy a nejinak tomu je u mixinu. Vzhledem k tomu, že nejčastější implementace mixinu je přes vícenásobnou dědičnost, tak v těchto případech často mixiny přebírají problémy s ní spojené (např. "diamantový problém" a to, že chování třídy je závislé na pořadí, v jakém je ze tříd děděno). Kvůli tomu např. Paul de Vrieze považuje mixiny za chybu v návrhu:

I find the concept of mixins to be a design flaw. The problems with mixins are that you are combining two types that do not know of eachother. The effect is similar to that of multiple inheritance, but without the explicit relation multiple inheritance implies. Multiple inheritence of course also has it's share of problems, but one can make the language such that it fails on encountering them, or requiring explicit resolves. Mixin's introduce unexpected behaviour on unsuspecting code that does not know about the combination and the issues that can be caused.

Také lze argumentovat tím, že při použití mixinů se jedná o zneužití dědičnosti (při dědění mixinů nevzniká relace generalizace/specializace). Dalším názorem je, že mixiny jsou kontroverzní, protože se je lidé snažít používat v jazycích, které pro ně nemají podporu:

Mixins are only 'controversial' because people try to use them in languages with no support for them. This makes all kinds of problems.

I přesto vidím v mixinech přínos. Nicméněje třeba mít na paměti, že každou techniku či vzor lze zneužít, takže i u Mixinu je třeba se před jejich použitím zamyslet, zda je to opravdu to, co potřebuji, a zda mi to nepřidá více problémů, než užitku...

Obsah tohoto pole je soukromý a nebude veřejně zobrazen.

Filtrované HTML (využíváno)

  • Povolené HTML značky: <a href hreflang> <em> <strong> <cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <table>
  • Zvýraznění syntaxe kódu lze povolit přes následující značky: <code>, <blockcode>, <bash>, <c>, <cpp>, <haskell>, <html>, <java>, <javascript>, <latex>, <perl>, <php>, <python>, <ruby>, <rust>, <sql>, <text>, <vim>, <xml>, <yaml>.
  • Řádky a odstavce se zalomí automaticky.
  • Webové a e-mailové adresy jsou automaticky převedeny na odkazy.
CAPTCHA
3 + 7 =
Vyřešte tento jednoduchý matematický příklad a vložte výsledek. Např. pro 1+3 vložte 4.
Nějak se mi tady rozmohl spam, takže poprosím o ověření.