Mixiny w językach programowania

O ile takie pojęcia jak polimorfizmdziedziczenie czy agregacja są doskonale znane adeptom programowania obiektowego, o tyle świadomość istnienia mixinów nie wydaje się być już tak powszechna.

Spotykane niekiedy polskojęzyczne określenie to domieszka. Oryginalna nazwa wywodzi się od lodziarni działającej w amerykańskim Somerville, w której serwowano desery w dość oryginalny sposób – klientom oferowano kilka podstawowych smaków lodów, do których mogli oni dobrać sobie różne dodatki, takie jak ciasteczka, cukierki czy bakalie. Powstały w ten sposób produkt został przez twórcę nazwany właśnie mix-in [1].

Oryginalne mixiny, czyli lody z domieszkami

Czym więc są owe mixiny w kontekście języków programowania? W pewnym uproszczeniu można je rozumieć jako interfejsy z zaimplementowanymi metodami. Ściślej mówiąc, są to klasy posiadające metody stworzone z myślą o użytku przez klasy pochodne ale bez istnienia typowej dla dziedziczenia relacji specjalizacji. Spróbuję wyjaśnić to na typowym w tego typu sytuacjach przykładzie, czyli zwierzętach. Załóżmy, że w naszym programie mamy m.in. takie klasy jak Pies, Nietoperz oraz Papuga. Dwie pierwsze z nich dziedziczą po klasie Ssak, Papuga rozszerza klasę Ptak, z kolei obie klasy reprezentujące gromady dziedziczą po klasie nadrzędnejZwierze. Relacje te przedstawia poniższy diagram:

 

Hierarchia klas zwierząt - dziedziczenie bez użycia mixinów

 

Tak zaprojektowany układ klas prawidłowo odzwierciedla świat rzeczywisty. Nietoperz jest ssakiem, papuga jest ptakiem i tak dalej – wszystko się zgadza. Wyobraźmy sobie jednak, że pisząc nasz program, zostajemy postawieni przed koniecznością zaimplementowania zestawu funkcjonalności cechujących tylko zwierzęta latające.

I w tym właśnie momencie na scenę wkraczają mixiny pod postacią klasy Latające. Klasa ta będzie zawierać funkcje związane z lataniem, a Nietoperz oraz Papuga będą te funkcje dziedziczyć. Warto przy tym zauważyć, że tworzenie instancji samego mixina nie ma za bardzo sensu, a więc przypomina on klasę abstrakcyjną. Różnica między tymi bytami jest przede wszystkim kwestią semantyki – klasa abstrakcyjna znajduje się z klasą pochodną w relacji specjalizacji (A JEST B), natomiast mixin tylko dostarcza pewną (czasami opcjonalną) funkcjonalność [2][3].

Kiedy używać mixinów?

Wyróżnić można co najmniej kilka przypadków, w których wykorzystanie mixinów wydaje się rozsądnym rozwiązaniem:

  • Jedną funkcjonalność trzeba w podobny sposób dodać do wielu różnych klas
  • Do jednej klasy trzeba dostarczyć wiele opcjonalnych funkcjonalności
  • W klasie trzeba zaimplementować wiele funkcjonalności, przy czym są one od siebie na tyle niezależne, że każda z nich może być reprezentowana przez osobną klasę

Załóżmy, że w kilkunastu klasach potrzebujemy dopisać serializację do formatu XML. Być może da się wydzielić kod, który będzie odpowiedzialny za tę akcję do osobnej klasy. Nie tylko zyskamy na czytelności, ale też unikniemy w ten sposób potencjalnych duplikacji kodu. Ciekawy przykład użycia mixinów można znaleźć w pythonowym pakiecie Werkzeug, w którym możemy w ten sposób dostosowywać do swoich potrzeb np. klasę reprezentującą request HTTP. Jeśli będziemy chcieli dołączać do żądań atrybut user-agent, możemy użyć takiego kodu:

Natomiast jeśli będziemy potrzebować trochę bardziej rozbudowanych obiektów, w niebywale łatwy sposób możemy dorzucić konieczne funkcjonalności:

Pierwsze domieszki

Za język, w którym mixinpojawiły się po raz pierwszy uznaje się Flavors – zorientowany obiektowo język programowania należący do rodziny Lispa. Nieco później w oparciu o Flavors powstał CLOS (Common Lisp Object System), będący zorientowanym obiektowo rozszerzeniem do Common Lispa. W jednej z pierwszych publikacji dotyczących mixinów, „Mixin-based Inheritance” autorstwa Gilada Branchy i Williama Cooka, pojawiają się przykłady używające właśnie CLOS [4].

W początkowej wersji programu mamy trzy klasy – Doctor i Graduate oraz nadrzędną wobec nich Person. Chcąc utworzyć nowy typ – lekarza ze stopniem naukowym musimy skorzystać z wielokrotnego dziedziczenia:

Problem, który tu występuje to tak zwany diamentowy problem – dziedziczymy po dwóch klasach, mających tę samą klasę nadrzędną. Oczywiście język ma zaimplementowane algorytmy służące rozwiązaniu tej sytuacji (CLOS używa tu dość popularnej linearyzacji, o której z pewnością jeszcze kiedyś napiszę), jednak nie jest to podejście idealne i bywa krytykowane m.in. za naruszanie enkapsulacji poprzez zmianę relacji dziedziczenia przy tym procesie [4]. Gdybyśmy jednak użyli mixina, nieco upraszczamy tę sytuację:

Jak można zauważyć, mixiny są tu tylko pewną konwencją programistyczną, nie posiadającą formalnego statusu. Ich charakterystyczną cechą w CLOS-ie jest m.in. fakt użycia funkcji call-next-method, która zapewnia dostęp do kolejnej metody w łańcuchu dziedziczenia. Tylko, że… Graduate-mixin po niczym przecież nie dziedziczy, prawda? Błędem byłaby więc próba stworzenia instancji tej klasy – jasno widać, że służy ona tylko do tego, aby wpleść ją do jakiejś innej klasy.

Mixiny w różnych językach

Zasadniczo domieszek można używać w tych językach, które posiadają możliwość dziedziczenia wielobazowego (wielokrotnego). W niektórych jest to kwestia jedynie konwencji i semantyki, w innych mixiny są nieco bardziej sformalizowane i wyróżnione jako specjalna funkcjonalność. Takie podejście zastosowali np. twórcy preprocesora Sass, w którym mixiny oznacza się bezpośrednio (przykład z oficjalnej strony Sass):

Zupełnie inaczej rzecz ma się we wspomnianym już w Pythonie, w którym mixinów w żaden sposób nie oznacza się na poziomie ich definicji, a jedyna różnica polega w odmiennym zastosowaniu, np.:

Gdyby chcieć przełożyć powyższy kod na język C++, robiąc to w sposób dosłowny, raczej nie wyszłoby z tego nic dobrego – dziedziczenie wielobazowe rządzi się tu bowiem nieco innymi prawami. Z tego powodu przy tworzeniu mixinów w C++ korzysta się na ogół z idiomu zwanego CRTP (Curiously Recurring Template Pattern), w którym dziedziczenie następuje po przekazanym klasie parametrze szablonowym, co w dużym uproszczeniu może wyglądać jak poniżej [5]:

Gdybyśmy chcieli, aby nasz nietoperz był tworzony przy użyciu kilku mixinów nic nie stoi na przeszkodzie aby to zrobić, mniej więcej tak:

Podsumowanie

Tworząc oprogramowanie w sposób zorientowany obiektowo warto być świadomym istnienia jak największej liczby wzorców, konceptów czy idiomów, dzięki którym możemy tworzyć kod jak najlepszej jakości. Tylko takie podejście, w którym programista nie jest ograniczony jedynie do kilku znanych mu rozwiązań (w tym przypadku: zwykłego dziedziczenia), pozwala na wybór prawdziwie najwłaściwszej dla danego problemu metody.

Linki i źródła

  1. Wikipedia: Mixin.
  2. StackOverflow: What is a mixin, and why are they useful?
  3. StackOverflow: What is the difference between an Abstract Class and a Mixin?
  4. Mixin-based Inheritance, G. Bracha, W. Cook, 1990.
  5. StackOverflow: What are Mixins (as a concept).