Skip to content

Groovy – Trait

Wstęp

Jako takiego wpisu na temat samego Groovy’ego do tej pory nie było. Ten jest pierwszym, w którym poruszam w ogóle tę tematykę. Sam właśnie zacząłem swoją przygodę z tą technologią i chciałbym Wam nieco ją przybliżyć, porządkując tym samym informacje, które staram się zapamiętać. Prawdopodobnie w niedalekiej przyszłości pojawi się jeszcze parę wpisów traktujących o tej tematyce.

Czym jest trait?

UWAGA: Nie będę w tekście tłumaczył nazwy własnej tej struktury.

Trait jest strukturalnym elementem języka, który pozwala nam na:

  • Tworzenie kompozycji zachowań
  • Implementację interfejsów na poziomie runtime’owym
  • Przeciążanie zachowań
  • Kompatybilność ze statycznym sprawdzaniem typowania oraz kompilacji

Możemy traktować je jako interfejsy, które potrafią zawierać zarówno stan jak i implementację. Do ich definiowania używamy słowa kluczowego trait.

Przykładowo może wyglądać to następująco:

trait ReptileTrait {
    String describeYourself() {
        "I'm a reptile!"
    }

    void giveVoice() {
        println "I don't know who am I, but I know my origin - I'm a reptile!"
    }
}

Po co ich używać?

Główną zaletą struktury tego typu jest m.in. możliwość pozbycia się tzw. problemu diamentu, związanego z wielokrotnym dziedziczeniem (które jest niemożliwe w Javie). Możemy w dosyć wygodny sposób panować nad strukturą kodu i sposobem jego wykonania przy nawet stosunkowo skomplikowanej jego strukturze, w której często wykorzystywany jest mechanizm dziedziczenia.

Struktura projektu

Wpis ten będzie bazował na prostym projekcie utworzonym w IntelliJ IDEA, który finalnie będzie wyglądać następująco:

Problem diamentu

Myślę, że jest on znany każdemu kto programuje w Javie. W razie gdyby ktoś nie wiedział o co chodzi, to odsyłam do wikipedii.

Załóżmy, że mamy następującą klasę:

package pl.lukaszpusz.mammals

import pl.lukaszpusz.traits.MammalTrail
import pl.lukaszpusz.traits.ReptileTrait

/**
 * Created by Lukasz Pusz on 08.07.2017.
 * www.lukaszpusz.pl
 */
class Dog implements ReptileTrait, MammalTrail {}

Jak widzimy implementuje ona dwie następujące struktury:

package pl.lukaszpusz.traits

/**
 * Created by Lukasz Pusz on 08.07.2017.
 * www.lukaszpusz.pl
 */
trait ReptileTrait {
    String describeYourself() {
        "I'm a reptile!"
    }

    void giveVoice() {
        println "I don't know who am I, but I know my origin - I'm a reptile!"
    }
}

oraz

package pl.lukaszpusz.traits

/**
 * Created by Lukasz Pusz on 08.07.2017.
 * www.lukaszpusz.pl
 */
trait MammalTrail {
    String describeYourself() {
        "I'm a mammal!"
    }

    void giveVoice() {
        println "I don't know who am I, but I know my origin - I'm a mammal!"
    }
}

Posiadają one dwie metody o identycznych sygnaturach. Można powiedzieć, że są odpowiednikami klasycznych interfejsów reprezentujących ssaki oraz gady, aczkolwiek posiadają własne implementacje (kwestia interfejsów w Javie 8 pojawia się nieco niżej). Pomijam sens implementowania interfejsu reprezentującego gada przez psa, aczkolwiek zostało to zrobione specjalnie. Klasa, która w swojej liście implementacji interfejsów posiada wiele różnych trait'ów, będzie domyślnie korzystać z implementacji tego, który znajduje się na samym końcu! Nie musimy się więc martwić, z której wersji skorzysta obiekt klasy DogWywołajmy więc w naszej metodzie main następującą metodę:

    def static diamondProblem() {
        def doggo = new Dog()
        println doggo.describeYourself() + "\n"
    }

Rezultat będzie następujący:

 

Rezultat zastosowania metody diamondProblem()

Jak więc widzimy, wywołana została implementacja metody z MammalTrait ,pomimo równoczesnej implementacji ReptileTrait !

Sterowanie zachowaniem

Wyobraźmy sobie sytuację, w której chcemy ‚ręcznie sterować’ zachowaniem z powyższego akapitu. W tym celu, stwórzmy następującą klasę:

package pl.lukaszpusz.reptiles

import pl.lukaszpusz.traits.MammalTrail
import pl.lukaszpusz.traits.ReptileTrait

/**
 * Created by Lukasz Pusz on 08.07.2017.
 * www.lukaszpusz.pl
 */
class Snake implements ReptileTrait, MammalTrail {}

Jak widzimy implementuje ona najpierw ReptileTrait , a następnie MammalTrait (dla ssaka w kodzie znajduje się literówka, wybaczcie). Chcielibyśmy, aby jednak mimo wszystko domyślnie wywołać pewną metodę z trait'u  będącego na innej pozycji w liście niż ostatnia. W tym celu w przeciążoną wersję metody describeYourself()  możemy stworzyć w następujący sposób:

    @Override
    String describeYourself() {
        ReptileTrait.super.describeYourself()
    }

Jak widzimy, na rzecz ReptileTrait , użyliśmy słowa kluczowego super . Z kolei na jego rzecz wywołaliśmy metodę describeYourself() . Stwórzmy teraz w naszej klasie Main następującą metodę:

    def static desiredBehaviour() {
        def snake = new Snake()
        println snake.describeYourself() + "\n"
    }

Rezultat jej wywołania będzie oczywiście następujący:

Implementacja trait’u z poziomu Runtime

Groovy wspiera dynamiczną implementację na poziomie runtime’u. Pozwala nam to na pewnego rodzaju ‚rozbudowanie’ funkcjonalności tworzonego przez nas klasy. Przykładowo stwórzmy sobie następującą klasę:

package pl.lukaszpusz.reptiles

/**
 * Created by Lukasz Pusz on 08.07.2017.
 * www.lukaszpusz.pl
 */
class WeirdDog {
    String describeMyself() {
        "Wraaaa! I'll eat u, little human! <T-Rex face emoji>"
    }
}

Jak widzimy zawiera ona w sobie tylko jedną metodę i nie implementuje żadnych interfejsów, ani także nie dziedziczy po innych klasach. W normalnym przypadku na rzecz jej obiektów moglibyśmy więc wywołać tylko tą jedną metodę. Stwórzmy sobie jednak w naszym maine następującą metodę:

    def static traitAtRuntime() {
        def weirdDoggo = new WeirdDog() as ReptileTrait

        println weirdDoggo.describeYourself()
        println weirdDoggo.describeMyself() + "\n"
    }

Zwróćmy uwagę na sposób w jaki stworzony został obiekt klasy WeirdDog. Użyte zostało słowo kluczowe as. W ten sposób dynamicznie rozbudowaliśmy funkcjonalność naszej klasy o możliwości oferowane przez ReptileTrait. Jak widzimy bez problemu możemy wywołać metody z obu tych struktur:

Trait vs Java 8 interface

Generalnie zachowanie omawianej w tym poście struktury jest bardzo podobne do tego, które opcjonalnie przedstawiają interfejsy od Javy w wersji 8. Występuje jednak między nimi jedna duża, zasadnicza różnica:

Implementacja ze struktury typu trait będzie zawsze wykorzystana, gdy jest on implementowany przez pewną klasę, która nie posiada własnej wersji implementacji, a także jednocześnie dziedziczy po pewnej klasie, która już takową implementację posiada!

Pokażmy to więc na przykładzie. Stwórzmy następujące klasy mające wspólną klasę nadrzędną oraz implementują ten sam trait :

package pl.lukaszpusz.reptiles

import pl.lukaszpusz.Animal
import pl.lukaszpusz.traits.ReptileTrait

/**
 * Created by Lukasz Pusz on 08.07.2017.
 * www.lukaszpusz.pl
 */
class Turtle extends Animal implements ReptileTrait {}

oraz

package pl.lukaszpusz.reptiles

import pl.lukaszpusz.Animal
import pl.lukaszpusz.traits.ReptileTrait

/**
 * Created by Lukasz Pusz on 08.07.2017.
 * www.lukaszpusz.pl
 */
class Dinosaur extends Animal implements ReptileTrait {}

Jak widzimy obie dziedziczą klasę Animal :

package pl.lukaszpusz

/**
 * Created by Lukasz Pusz on 08.07.2017.
 * www.lukaszpusz.pl
 */
class Animal {
    void giveVoice() {
        println "I'm an implementation of giveVoice() from Animal class"
    }
}

oraz implementują ReptileTrait (pokazany powyżej). Stwórzmy więc w naszym mainie następującą metodę:

    def static traitsVsJava8Interfaces() {
        def dinosaur = new Dinosaur()
        def turtle = new Turtle()

        dinosaur.giveVoice()
        turtle.giveVoice()

        println()
    }

i ją wywołajmy:

Jak widzimy, została więc wykonana wersja implementacji z ReptileTrait , a nie klasy nadrzędnej!

Dynamiczny kod

Trait może wywołać dowolny dynamiczny kod – jak każdą inną, normalną klasę. Oznacza to, że można w ciele metody wywołać metodę, która będzie istnieć w klasie implementującej nasz trait, aczkolwiek nie będzie posiadać jawnej deklaracji w traicie. Stwórzmy więc następujące struktury:

package pl.lukaszpusz.traits

/**
 * Created by Lukasz Pusz on 08.07.2017.
 * www.lukaszpusz.pl
 */
trait SpeakingDuck {
    String speak() { helloGroovy() }
}

oraz

package pl.lukaszpusz.mammals

import pl.lukaszpusz.traits.SpeakingDuck

/**
 * Created by Lukasz Pusz on 08.07.2017.
 * www.lukaszpusz.pl
 */
class Duck implements SpeakingDuck {
    def methodMissing(String name, args) {
        "${name.capitalize()}!"
    }
}

Następnie stwórzmy w mainie następującą metodę:

    def static dynamicCode() {
        def duck = new Duck()
        println duck.speak() + "\n"
    }

Rezultat będzie następujący:

Jak widzimy wyświetlona została nazwa metody podanej jako parametr metody speak()  w SpeakingDuck . Stało się tak dzięki implementacji metody methodMissing() , która dynamicznie wyciągnęła interesującą nas treść. Zostaje ona (jak sama nazwa wskazuje) w momencie, gdy odwołujemy się do metody, która jawnie nie jest nigdzie zadeklarowana (moglibyśmy powiedzieć, że nie istnieje).

Metody dynamiczne

Możliwym jest również implementowanie w traicie metod typu MOP (https://www.techopedia.com/definition/31833/method-of-procedure-mop). Przykładem takiej metody jest właśnie pokazana powyżej methodMissing() . Stwórzmy więc sobie pokazany poniżej trait:

package pl.lukaszpusz.mammals

/**
 * Created by Lukasz Pusz on 08.07.2017.
 * www.lukaszpusz.pl
 */
trait DynamicDuck {
    private Map props = [:]

    def methodMissing(String name, args) {
        name.toUpperCase()
    }
    def propertyMissing(String prop) {
        props[prop]
    }
    void setProperty(String prop, Object value) {
        props[prop] = value
    }
}

oraz następującą klasę:

package pl.lukaszpusz.mammals

import pl.lukaszpusz.traits.DynamicDuck

/**
 * Created by Lukasz Pusz on 08.07.2017.
 * www.lukaszpusz.pl
 */
class FastDuck implements DynamicDuck {
    String existingProperty = "ok"
    String existingMethod() { "ok" }
}

Ich działanie przetestujemy w klasie mainie, tworząc następującą metodę:

    def static dynamicMethodsInTrait() {
        def fastDuck = new FastDuck()

        println fastDuck.existingProperty
        println fastDuck.foo

        fastDuck.foo = "bar"
        println fastDuck.foo
        println fastDuck.existingMethod()
        println fastDuck.someMethod()

    }

Jak widzimy w pierwszej kolejności tworzymy zwykły obiekt. Później kolejno drukujemy pola existingProperty  oraz foo . Dla pierwszych dwóch linii rezultat wykonania println będzie wyglądać następująco:

„ok” jest faktyczną wartością, którą podaliśmy w klasie FastDuck . Natomiast wartość null jest rezultatem próby wyświetlenia pola, które tak naprawdę nie zostało jawnie zadeklarowane! Za przypisanie jej wartości odpowiada metoda propertyMissing() znajdująca się w DynamicDuck! Przyjrzyjmy się jednak co można zaobserwować poniżej:

Jak widzimy mogliśmy ustawić wartość pola, które nie było jawnie zadeklarowanie, a następnie się do niego odwołać i nadać mu wartość. Następny wyraz „OK” jest niczym innym, jak wywołaniem zwykłej metody klasy FastDuck . Ostatnia linia, a więc „SOMEMETHOD” jest nazwą niezadeklarowanej metody someMethod()  wywołanej na rzecz obiektu fastDuck .

Wnioski

Cała struktura wydaje się być bardzo lekka i przyjazna w użyciu, aczkolwiek jest narzędziem zdecydowanie potężnym, które daje ogromne ogromne możliwości. Oczywiście nie jest to mechanizm niezastąpiony, bo jak wiemy w tradycyjnej Javie (różnice zmniejszają się od wersji 8) pomimo pewnych różnych różnic i tak jesteśmy w stanie osiągnąć podobne efekty, aczkolwiek w nieco inny sposób.  W tym wpisie omówiłem tylko pewną część specyfikacji struktury jaką jest trait  oferowany nam w języku Groovy.


Źródła:

W najbliższym czasie cały projekt pojawi się na moim githubie.

Facebooktwitterredditlinkedinmail
Published inJava