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 pomidor
trait 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) //rectangle
No 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