Poprzednie części:

Początkowo chciałem włączyć temat tworzenia własnych wzorców do poprzedniego wpisu. Jednak ze względu na konieczność zrobienia kilku rzeczy, bez których będzie nam ciężko, postanowiłem wyłączyć ten temat do osobnego wpisu.

Dodatkowe zależności

Samodzielne pisanie wzorców jest uciążliwe i w sumie mało przyjemne. Javaslang jest jednak oparty o generatory, a zatem pozwólmy robić im ich robotę. W tym celu musimy dodać jedną zależność w naszym pliku build.gradle:

Listing 1. Konfiguracja projektu

apply plugin: 'java'

repositories {
    jcenter()
    mavenCentral()
}

sourceSets {
    generated {
        java {
            srcDirs = ['src/main/java']
        }
    }
}

configurations {
    patternMatch
}

dependencies {

    compile 'org.slf4j:slf4j-api:1.7.21'
    compile('io.javaslang:javaslang:2.0.2')
    patternMatch 'io.javaslang:javaslang-match:2.0.2'

    testCompile 'junit:junit:4.12'
}

task generatePatterns(type: JavaCompile, group: 'build', description: 'Generates patterns classes') {

    source = sourceSets.main.java 
    classpath = configurations.compile + configurations.patternMatch
    options.compilerArgs = [
            "-proc:only",
            "-processor", "javaslang.match.PatternsProcessor"
    ]
    destinationDir = sourceSets.generated.java.srcDirs.iterator().next()
}

compileJava {
    dependsOn generatePatterns

    options.compilerArgs =[
            "-proc:none"
    ]
}

compileGeneratedJava {
    dependsOn generatePatterns
    options.warnings = false
    classpath += sourceSets.main.runtimeClasspath
}

Biblioteka javaslang-match zawiera procesor adnotacji, który wygeneruje nam odpowiednie klasy. Jednak by prawidłowo działał, należy:

  • Stworzyć dodatkowy task, który zajmie się generowaniem klas.
  • Wyłączyć procesory adnotacji w głównym cyklu kompilacji.

Pierwsze robimy poprzez dodanie zadania generatePatterns oraz konfiguracji patternMatch. Drugie za pomocą dodania argumentu -proc:none do wywołania kompilatora. Powyższa konfiguracja to kombinacją braku możliwości konfiguracji generatora i mojej niewiedzy w zakresie konfigurowania Gradle.

Model

Tu sprawa jest prosta. Będziemy używać klasy User, która zawiera trzy pola:

Listing 2. Klasa User

public class User {

    private String name;
    private String email;
    private String role;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getRole() {
        return role;
    }

    public void setRole(String role) {
        this.role = role;
    }
}

Ot zwykłe POJO. Równie dobrze możemy użyć data class z Kotlina. Jednak jest jeden warunek. Jeżeli nie chcemy samodzielnie kompilować Javaslang, to musimy ograniczyć się do maksymalnie ośmiu pól w dekompozycji obiektu. Tyle właśnie wynosi największa pojemność krotki w Javaslang.

Własny wzorzec

Tworzenie własnego wzorca rządzi się prostymi zasadami. Po pierwsze musimy stworzyć klasę, która będzie zawierać wzorce. Oznaczymy ją adnotacją @Patterns.

Listing 3. Klasa Users

@Patterns
public class Users {
 
}
T’n’T z nazwami

Generator utworzy klasę UsersPatterns. Dlaczego nie możemy utworzyć klasy User? Jest bug w Javaslang, który powoduje, że generator nie używa pełnych nazw klas wraz z pakietami. W efekcie w pewnym momencie mamy sytuację jak poniżej:

Listing 4. Błędnie wygenerowana klasa

package pl.koziolekweb.javaslang.patternmatching.patterns;

import javaslang.API.Match.Pattern;
import javaslang.API.Match.Pattern3;
import pl.koziolekweb.javaslang.patternmatching.User;

// GENERATED BY JAVASLANG <<>> derived from pl.koziolekweb.javaslang.patternmatching.patterns.User

public final class UserPatterns {

    private UserPatterns() {
    }

    public static <_1 extends String, _2 extends String, _3 extends String> Pattern3<User, _1, _2, _3> User(Pattern<_1, ?> p1, Pattern<_2, ?> p2, Pattern<_3, ?> p3) {
        return Pattern3.of(User.class, p1, p2, p3, User::User);
    }

}

Problemem jest zapis User::User, który odwołuje się do naszej klasy ze wzorcami. Jednocześnie mamy zaimportowaną klasę pl.koziolekweb.javaslang.patternmatching.User, co prowadzi do konfliktu nazw. W efekcie mamy błąd kompilacji…

Zgłosiłem buga. Zobaczymy co z nim zrobią. Rozwiązali go.

Dekonstrukcja

Kolejnym krokiem jest dodanie metody, która będzie dekonstruować obiekt User do Tuple3. Z dekonstrukcją na poziomie semantyki języka mieliśmy już do czynienia w Kotlinie. Choć brzmi to groźnie, to sama implementacja jest banalnie prosta:

Listing 5. Metoda User służy do dekonstrukcji

@Patterns
public class Users {
 
    @Unapply
    static Tuple3<String, String, String> User(pl.User user) {
        return Tuple.of(user.getName(), user.getEmail(), user.getRole());
    }

}

Metoda musi mieć adnotację @Unapply i musi być statyczna. Możemy używać naszego wzorca:

Listing 6. Użycie User w dopasowaniu

public void custom() {
    User user = new User("Jan Kowalski", "jan@kowalski", "Jan");

    System.out.println(
            Match(user).of(
                    Case(User($(), $(), $("Jan")), "OK - Jan"),
                    Case(User($(), $(), $("Emil")), "OK - Emil")
            )
    );
}

W efekcie mamy możliwość dopasowania obiektu do wzorca według samodzielnie zdefiniowanych reguł.

Podsumowanie

Tworzenie własnych wzorców i reguł dopasowania nie jest trudne. Najwięcej czasu zajęło mi odpowiednie skonfigurowanie gradle tak by generowane klasy były widoczne w projekcie. To jest jakaś porażka niestety.