O ile takie pojęcia jak polimorfizm, dziedziczenie 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].
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:
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].
Spis treści
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:
1 2 3 4 |
from werkzeug.wrappers import UserAgentMixin, BaseRequest class Request(UserAgentMixin, BaseRequest): pass |
Natomiast jeśli będziemy potrzebować trochę bardziej rozbudowanych obiektów, w niebywale łatwy sposób możemy dorzucić konieczne funkcjonalności:
1 2 3 4 |
from werkzeug.wrappers import UserAgentMixin, AcceptMixin, ETagRequestMixin, AuthorizationMixin, BaseRequest class Request(UserAgentMixin, AcceptMixin, ETagRequestMixin, AuthorizationMixin, BaseRequest): pass |
Pierwsze domieszki
Za język, w którym mixiny pojawił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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
(defclass Person () (name)) (defmethod display ((self Person)) (display (slot-value self ’name))) (defclass Graduate (Person) (degree)) (defmethod display ((self Graduate)) (call-next-method) (display (slot-value self ’degree))) (defclass Doctor (Person) ()) (defmethod display ((self Doctor)) (display “Dr. ”) (call-next-method)) (defclass Research-Doctor (Doctor Graduate) ()) |
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ę:
1 2 3 4 5 6 |
(defclass Graduate-mixin () (degree)) (defmethod display ((self Graduate-mixin)) (call-next-method) (display (slot-value self ’degree))) (defclass Research-Doctor (Graduate-mixin Doctor) ()) |
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):
1 2 3 4 5 6 7 8 9 |
@mixin square($size, $radius: 0) width: $size height: $size @if $radius != 0 border-radius: $radius .avatar @include square(100px, $radius: 4px) |
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.:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Animal(object): def __init__(self): self.is_moving = False class FlyingMixin(object): def fly(self): self.is_moving = True class WalkingMixin(object): def walk(self): self.is_moving = True class Bat(FlyingMixin, Animal): pass class Dog(WalkingMixin, Animal): pass pluto = Dog() pluto.walk() print(pluto.is_moving) |
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]:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <iostream> struct Animal { bool isMoving; }; template<typename T> struct FlyingMixin : public T { virtual void fly() { this->isMoving = true; } }; struct Bat : FlyingMixin<Animal> { }; int main() { Bat bat; bat.fly(); std::cout << bat.isMoving << std::endl; } |
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:
1 |
typedef TwoLegsMixin< FlyingMixin<Animal> > Bat; |
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
- Wikipedia: Mixin.
- StackOverflow: What is a mixin, and why are they useful?
- StackOverflow: What is the difference between an Abstract Class and a Mixin?
- Mixin-based Inheritance, G. Bracha, W. Cook, 1990.
- StackOverflow: What are Mixins (as a concept).