Zagadnienie, które będzie poniżej plastycznie opisane kwiecistym językiem ma się tak do codziennego programowania jak zawody formuły 1 do jazdy osobówką po mieście. Niby nie widzimy na ulicach formułek a jednak stanowią one ciekawy poligon dla rozwiązań technicznych, które po pewnym czasie lądują w normalnych samochodach
Z drugiej jednak strony fakt, że nie mam pojęcia o zawodach formuły 1 może sprawić, że powyższe porównanie jest nieprawdziwe i bez sensu. Tak czy inaczej najtrudniejszą część - czyli wstęp - mam już za sobą i można jechać dalej...
Więcej niż "polimorfizm"
Przez lata moje rozumienie słowa "polimorfizm" sprowadzało się do "jest interfejs i jego kilka implementacji" i było swoistym uwiecznieniem dobrego projektu obiektowego. Pierwsza ciekawa nowinka(dla mnie) - można to samo uzyskać bez żadnego interfejsu zwykłymi generykami - a dwa - nie ma się czym podniecać bo jak wszystko w życiu ma to swoje wady i zalety.
Na wzmiankę o wadach takiego rozwiązania natrafimy w miejscu, którego się nie do końca spodziewamy - czyli w rozdziale zdaje się szóstym "Clean Kodu" o Obiektach i Strukturach danych. Właśnie tam opisany jest ciekawy przypadek kiedy to użycie niesławnego instanceof może nam bardziej pomóc niż zaszkodzić. Gdy mamy sytuację gdzie dochodzą dochodzą nam nowe typy to rozwiązanie z interfejsem ładnie w to wchodzi ale już przy nowych operacjach, na typach jednak wbrew intuicji i powszechnej poprawności projektowej - instance of sprawdza się lepiej.
I to jest moment na wzmiankę o ...
Expression problem
Genezę problemu można sobie znaleźć tutaj -> http://en.wikipedia.org/wiki/Expression_problem lub tutaj : Expression Problem. Co więcej możemy legalnie ściągnąć profesjonalną pracę naukową samego Oderskiego -> PDF Oderskiego
Niestety albo stety prace naukowe mają do siebie, że brak tam rysunków i animacji rodem z "head first" albo też multimedialnych książek kucharskich. Aby więc było łatwiej zamiast używać oryginalnego problemu z rozwijaniem języka wyrażeń naukowych skupimy się na przykładzie z "Clean Code" - kółka i kwadraty (bo "trójkąty i kwadraty" to już chyba będzie plagiat).
- M1 - tworzymy interfejs kształt i klasę kółko
- M2 - dodajemy nowy typ kwadrat - tu nie ma schodów
- M3 - dodajemy nową operację na kółku - tu są schody
- M4 - a tutaj tę nową operację dodajemy też do kwadratu
M1
//1 object ExpressionsBlog extends M1{ //2 val circle:Shape=new Circle(5) //> circle : com.wlodar.expressions.poligon.ExpressionsBlog.Shape = com.wlodar. //| expressions.poligon.nablog.M1$Circle@72067564 } package nablog{ //3 trait M1{ trait Shape{ def area:Double } //4 class Circle(r:Int) extends Shape{ def area=Math.PI*r*r } //5 def sum(s1:Shape,s2:Shape)=s1.area+s2.area } }
- Traity w Scali najłatwiej wytłumaczyć porównując je do interfejsów w Javie ale tutaj widać także ich trochę inną naturę - w tym miejscu trait M1 wzbogaca moduł/obiekt ExpressionsBlog w nowe zachowanie i nowe typy(tutaj Shape i przykładowa implementacja Circle)
- Ponieważ kompilator Scali sam zgaduje typ zmiennej, więc aby uogólnić ów typ do Shape musimy jawnie zadeklarować kółko jako właśnie Shape
- Mamy dwa traity : M1 i Shape - widzimy (mam nadzieję) także dwoistą naturę tego konceptu - "Interfejs" i "Moduł". Możemy sobie wewnątrz deklarować klasy itd itp
- Klasa jak klasa - standardowe rozszerzenie Shape z implementacją metody
- A tutaj mamy metodę sum, która będzie służyła do ilustracji kilku ciekawych faktów i eksperymentów okołopolimorfizmowych.
M2
//1 trait M2 extends M1{ class Square(a:Int) extends Shape{ def area=a*a } } //2 trait M2Rectangle extends M2{ class Rectangle(a:Int,b:Int) extends Shape { def area=a*b } }
- Dodanie nowego typu jest naturalne i nie wymaga żadnego "wzorca" - co podkreślam by czytelnik dopuścił do siebie fakt, iż wzorzec pomimo "efektu aureoli" cudnego rozwiązania może być zwykłym nadmiarowym szumem
- No i Traity można rozwijać niezależnie
//1 object ExpressionsBlog extends M2 with M2Rectangle{ val circle:Shape=new Circle(5) //> circle : com.wlodar.expressions.poligon.ExpressionsBlog.Shape = com.wlodar. //| expressions.poligon.nablog.M1$Circle@57a220c2 //2 val square=new Square(3) //> square : com.wlodar.expressions.poligon.ExpressionsBlog.Square = com.wlodar //| .expressions.poligon.nablog.M2$Square@23557525 //2 val rectangle=new Rectangle(3,4) //> rectangle : com.wlodar.expressions.poligon.ExpressionsBlog.Rectangle = com. //| wlodar.expressions.poligon.nablog.M2Rectangle$Rectangle@164af41d }
- Przykład dodania dwóch traitów do modułu
- Dzięki temu mamy dostęp zarówno do kwadratu jak i prostokąta
M3
//1 trait M3 extends M1{ //2 trait Shape extends super.Shape{ def perimeter:Double } //3 class Circle(r:Int) extends super.Circle(r) with Shape{ def perimeter=2*Math.PI*r } }
- Przy dodawaniu nowej operacji zaczynają się schody - nie da się wykonać tego tak naturalnie jak w przypadku dodawania nowych typów
- Aby zrodzić iluzję dodanej metody przesłaniamy trait Shape z M1
- To samo robimy z klasą ale tutaj jeszcze dodatkowo implementujemy przesłonięty Shape
//1 object ExpressionsBlog extends M3 with M2Rectangle{ val circle:Shape=new Circle(5) //> circle : com.wlodar.expressions.poligon.ExpressionsBlog.Shape = com.wlodar. //| expressions.poligon.nablog.M3$Circle@331f9cee //2 circle.perimeter //> res0: Double = 31.41592653589793 circle.area //> res1: Double = 78.53981633974483 }
- Cały czas implementujemy dwa niezależne rozszerzenia M1 - za chwilę stanie się z tym coś ciekawego.
- Kółko dostało obydwie metody area i perimeter
M4
//1 trait M4 extends M2 with M3{ //2 class Square(a:Int) extends super.Square(a) with Shape{ def perimeter=4*a } }
- Scalamy oba kierunki w jeden moduł - tego chyba nie da się zrobić w Javie - nawet ósemce z "defaultowymi metodami" bo jest konflikt nazw
- Standardowe Przesłoniećie klasy - klasę bazową bierzemy z jednego modułu a nową operację z drugiego
Niebezpieczeństwo
W opracowaniu Oderskiego wykorzystany jest dodatkowo typ abstrakcyjny- na początku zupełnie nie rozumiałem po co skoro wszystko działało i bez tego. No i to był problem bo ... działało za bardzo. Zerknijmy na to co jest w M1
trait M1{ //.... def sum(s1:Shape,s2:Shape)=s1.area+s2.area }
I teoretycznie mogę tutaj przekazać coś co implementuje Shape z M1 jak i Shape z M3. Ale co jeśli to jest niedobre, bardzo bardzo złe i nie chcę tego? No to teraz czas włąśnie na...
Typ Abstrakcyjny
Aby pokazać pełną abstrakcję typu abstrakcyjnego nazwiemy go pomidortrait M1 { //... //1 type pomidor <: Shape //2 def sum(s1: pomidor, s2: pomidor) = s1.area + s2.area }
- Najtrudniejszym elementem w tym miejscu jest chyba ten znaczek : "<:" wzięty prosto z gry PACMAN - a oznacza on zwyczajnie, że pomidor będzie podtypem kształtu.
- Nasza metoda nie przyjmuje teraz kształtów ale dwa pomidory. Wydaje się, że za wiele zmian nie zobaczymy w porównaniu do zwykłego dziedziczenia ale teraz tak naprawdę mogę dokładnie sterować czym jest
pomidor- i jak będę chciał to nie będzie miał nic wspólnego z Shape z M1
rait M3 extends M1 { //1 trait Shape extends super.Shape { def perimeter: Double } //2 type pomidor = Shape //.... }
- Pamiętamy, że Shape z M1 jest przesłonięty przez nowy Shape
- I ustalamy, że pomidor to właśnie nasz nowy Shape czyli to co nie implementuje Shape z M3 nie ma wstępu do metody
object Testy extends M4 with M2Rectangle { def test = { val circle: Shape = new Circle(5) val rec = new Rectangle(3, 4) //rec i circle to dwa różne szejpy! sum(circle, rec) } }
Polimorfizm inaczej
Metoda sum zdefiniowana w M1 dodaje pola dwóch figur. Stwórzmy sobie sztuczny problem i załóżmy, że po dodaniu nowej metody w M3 chcielibyśmy mieć kontrolę nad tym jak to sum jest liczone :
trait M1 { //.... type pomidor <: Shape //2 trait Summarizer{ def add(s1:pomidor,s2:pomidor):Double } object AreaSummarizer extends Summarizer{ def add(s1:pomidor,s2:pomidor)=s1.area+s2.area } //1 def sum(s1: pomidor, s2: pomidor)(s:Summarizer) = s.add(s1,s2) }
- Definiujemy taką niby strategię - sposób dodawania pomidorów jest zdefiniowany poza metodą sum
- Summarizer to zwykły interfejs
trait M3 extends M1 { //................................ type pomidor = Shape object PerimeterSummarizer extends Summarizer{ def add(s1:pomidor,s2:pomidor)=s1.perimeter+s2.perimeter } } sum(circle, sq)(PerimeterSummarizer)Jest jeden mały problem - API troszeczkę ssie pałkę. Zobaczmy co można z tym zrobić :
//1 def sum(s1: pomidor, s2: pomidor)(implicit s:Summarizer) = s.add(s1,s2) //2 implicit val summarizer=PerimeterSummarizer //3 sum(circle, sq)
- Dodajemy słówko "implicit" co oznacza, że summarizer będzie wyszukiwany przez kompilator i jak go znajdzie to sam go wstawi jako parametr (a jak nie to będzie błąd). Uwagę czytelnika niech zwróci także druga para nawiasów.
- Gdzies tam w module deklarujemy domyślnego summarizera
- API jest znowu czyste - jak chcemy to cały czas możemy przekazać kolejny argument, który będzie różny od implicit
trait M1 { //.... type pomidor <: Shape //1 trait Summarizer[A]{ def add(s1:A,s2:A):Double } //2 object AreaSummarizer extends Summarizer[pomidor]{ def add(s1:pomidor,s2:pomidor)=s1.area+s2.area } //3 def sum[A](s1: A, s2: A)(implicit s:Summarizer[A]) = s.add(s1,s2) }
- Summarizer ma generyka A
- AreaSummarizer określa generyka A jako pomidor
- I sum ma tez generyka A
trait M3 extends M1 { //.... type pomidor = Shape //1 implicit object PerimeterSummarizer extends Summarizer[pomidor]{ def add(s1:pomidor,s2:pomidor)={println("pomidor");s1.perimeter+s2.perimeter} } } trait M2Rectangle extends M2 { //2 implicit val summarizer=new Summarizer[Rectangle]{ def add(s1:Rectangle,s2:Rectangle)={println("rectangle");s1.area+s2.area} } class Rectangle(a: Int, b: Int) extends Shape { def area = a * b } } def test = { val circle = new Circle(5) val rec = new Rectangle(3, 4) val sq = new Square(6) sum(circle, sq) // pomidor sum(rec, rec) // rectangle }Normalnie kompilator sam wstrzykuje to co trzeba na podstawie typu! W "1" mamy obsługę Shape w "2" prostokąta ze starym shape z M1 i wio. Ale jest jeszcze bardziej pojechany mechanizm...
Typy Fantomowe
Najpierw ogólna koncepcja ://1 package phantoms1{ sealed trait NotValidated sealed trait Validated sealed trait Normal sealed trait Admin case class User[A,B] } //2 def validate[B](u:User[NotValidated,B])=user.copy[Validated,B] def secret[B](u:User[Validated,B])=println("sekrety") //3 val user=new User[NotValidated,Normal] val validatedUser=validate(user) //4 secret(validatedUser) //> sekrety //5 secret(user)
- Deklarujemy serię traitów, która tak naprawdę nigdzie nie będzie deklarowana - dlatego własnie fantomy
- Tutaj jest istota przykładu - mamy taką niby walidację w czasie kompilacji poprzez sprawdzenie czy user ma odpowiednie generyki a może je mieć tylko wtedy jeśli przeszedł metodę "validate"
- Na początku user miał generyka NotValidated
- Jeśli przeszedł walidację i generyk zmienił się na Validated to wyswietli sekret
- A jak nie to wywali sie kompilacja
trait Summarizer[A,B]{ def add(s1:A,s2:A):Double } trait ZwyklySummarizer implicit object AreaSummarizer extends Summarizer[pomidor,ZwyklySummarizer]{ def add(s1:pomidor,s2:pomidor)={println("shape z M1");s1.area+s2.area} } def sum[A](s1: A, s2: A)(implicit s:Summarizer[A,ZwyklySummarizer]) = s.add(s1,s2) ////////// trait NiezwyklySummarizer implicit object PerimeterSummarizer extends Summarizer[pomidor,NiezwyklySummarizer]{ def add(s1:pomidor,s2:pomidor)={println("pomidor z M3");s1.perimeter+s2.perimeter} } //// implicit val summarizer=new Summarizer[Rectangle,ZwyklySummarizer]{ def add(s1:Rectangle,s2:Rectangle)={println("rectangle");s1.area+s2.area} } // sum(circle, sq) //shape z M1 sum(rec, rec) //rectangleNo i "NiezwykłySummarizer" z M3 został olany i w jego miejsce wszedł "Zwykly" z M1. Przykład trochę na siłę ale stworzony w celach czysto edukacyjnych.
Podsumowanie
- W programowaniu czeka na nas wiele ciekawych rzeczy
- Warto czasem czytać referaty i opracowania naukowe i zwalczyć naturalną niechęć do tego typu prac wykształcony w nas przez system szkolnictwa polskiego.
- a tu jest cały kod
* * *
Brak komentarzy:
Prześlij komentarz