Introduction à AutoValue

Introduction à AutoValue

1. Vue d'ensemble

AutoValue est un générateur de code source pour Java, et plus précisément une bibliothèque pourgenerating source code for value objects or value-typed objects.

Pour générer un objet de type valeur, tout ce que vous avez à faire est deannotate an abstract class with the @AutoValue annotation et de compiler votre classe. Ce qui est généré est un objet de valeur avec des méthodes d'accès, un constructeur paramétré, des méthodestoString(), equals(Object) ethashCode() correctement remplacées.

L'extrait de code suivant esta quick example d'une classe abstraite qui, une fois compilée, donnera un objet de valeur nomméAutoValue_Person.

@AutoValue
abstract class Person {
    static Person create(String name, int age) {
        return new AutoValue_Person(name, age);
    }

    abstract String name();
    abstract int age();
}

Continuons et apprenons-en plus sur les objets de valeur, pourquoi nous en avons besoin et comment AutoValue peut aider à réduire considérablement la tâche de génération et de refactorisation du code.

2. Maven Setup

Pour utiliser AutoValue dans un projet Maven, vous devez inclure la dépendance suivante dans lespom.xml:


    com.google.auto.value
    auto-value
    1.2

La dernière version peut être trouvée en suivantthis link.

3. Objets de type valeur

Les types de valeur sont le produit final de la bibliothèque. Par conséquent, pour apprécier sa place dans nos tâches de développement, nous devons bien comprendre les types de valeur, ce qu’ils sont, ce qu’ils ne sont pas et pourquoi nous en avons besoin.

3.1. Que sont les types de valeur?

Les objets de type valeur sont des objets dont l'égalité entre eux n'est pas déterminée par l'identité mais plutôt par leur état interne. Cela signifie que deux instances d'un objet de type valeur sont considérées comme égales tant qu'elles ont des valeurs de champ égales.

Typically, value-types are immutable. Leurs champs doivent êtrefinal et ils ne doivent pas avoir de méthodessetter car cela les rendra modifiables après instanciation.

Ils doivent consommer toutes les valeurs de champ via un constructeur ou une méthode d'usine.

Les types valeur ne sont pas des JavaBeans car ils n'ont pas de constructeur d'argument par défaut ou zéro et ils n'ont pas non plus de méthodes de définition, de même,they are not Data Transfer Objects nor Plain Old Java Objects.

En outre, une classe de type valeur doit être finale pour ne pas pouvoir être étendue, à moins que quelqu'un ne remplace les méthodes. Les JavaBeans, les DTO et les POJO ne doivent pas nécessairement être définitifs.

3.2. Création d'un type de valeur

En supposant que nous voulons créer un type valeur appeléFoo avec des champs appeléstext etnumber. Comment procéderions-nous?

Nous ferions une classe finale et marquerions tous ses champs comme final. Ensuite, nous utiliserions l'EDI pour générer le constructeur, la méthodehashCode(), la méthodeequals(Object), lesgetters comme méthodes obligatoires et une méthodetoString(), et nous aurions un classe comme ça:

public final class Foo {
    private final String text;
    private final int number;

    public Foo(String text, int number) {
        this.text = text;
        this.number = number;
    }

    // standard getters

    @Override
    public int hashCode() {
        return Objects.hash(text, number);
    }
    @Override
    public String toString() {
        return "Foo [text=" + text + ", number=" + number + "]";
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        Foo other = (Foo) obj;
        if (number != other.number) return false;
        if (text == null) {
            if (other.text != null) return false;
        } else if (!text.equals(other.text)) {
            return false;
        }
        return true;
    }
}

Après avoir créé une instance deFoo, nous nous attendons à ce que son état interne reste le même pendant tout son cycle de vie.

Comme nous le verrons dans la sous-sectionthe hashCode of an object must change from instance to instance suivante, mais pour les types valeur, nous devons le lier aux champs qui définissent l'état interne de l'objet valeur.

Par conséquent, même changer un champ du même objet changerait la valeur dehashCode.

3.3. Comment fonctionnent les types de valeur

La raison pour laquelle les types de valeur doivent être immuables est d'empêcher toute modification de leur état interne par l'application après leur instanciation.

Chaque fois que nous voulons comparer deux objets de type valeur,we must, therefore, use the equals(Object) method of the Object class.

Cela signifie que nous devons toujours remplacer cette méthode dans nos propres types de valeur et ne renvoyer true que si les champs des objets de valeur que nous comparons ont des valeurs égales.

De plus, pour que nous utilisions nos objets de valeur dans des collections basées sur le hachage commeHashSets etHashMaps sans casser,we must properly implement the hashCode() method.

3.4. Pourquoi nous avons besoin de types de valeur

Le besoin de types de valeur apparaît assez souvent. Ce sont des cas où nous aimerions remplacer le comportement par défaut de la classe d'origineObject.

Comme nous le savons déjà, l'implémentation par défaut de la classeObject considère deux objets égaux lorsqu'ils ont la même identité maisfor our purposes we consider two objects equal when they have the same internal state.

En supposant que nous souhaitons créer un objet monétaire comme suit:

public class MutableMoney {
    private long amount;
    private String currency;

    public MutableMoney(long amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }

    // standard getters and setters

}

Nous pouvons exécuter le test suivant pour tester son égalité:

@Test
public void givenTwoSameValueMoneyObjects_whenEqualityTestFails_thenCorrect() {
    MutableMoney m1 = new MutableMoney(10000, "USD");
    MutableMoney m2 = new MutableMoney(10000, "USD");
    assertFalse(m1.equals(m2));
}

Notez la sémantique du test.

Nous considérons qu'il est passé lorsque les deux objets de monnaie ne sont pas égaux. En effet,we have not overridden the equals method donc l'égalité est mesurée en comparant les références mémoire des objets, qui bien sûr ne seront pas différentes car ce sont des objets différents occupant des emplacements mémoire différents.

Chaque objet représente 10 000 USD maisJava tells us our money objects are not equal. Nous voulons que les deux objets ne testent l'inégalité que lorsque les montants en devise sont différents ou les types de devise sont différents.

Créons maintenant un objet de valeur équivalente et cette fois-ci, nous laisserons l'EDI générer la plupart du code:

public final class ImmutableMoney {
    private final long amount;
    private final String currency;

    public ImmutableMoney(long amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + (int) (amount ^ (amount >>> 32));
        result = prime * result + ((currency == null) ? 0 : currency.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        ImmutableMoney other = (ImmutableMoney) obj;
        if (amount != other.amount) return false;
        if (currency == null) {
            if (other.currency != null) return false;
        } else if (!currency.equals(other.currency))
            return false;
        return true;
    }
}

La seule différence est que nous avons remplacé les méthodesequals(Object) ethashCode(), maintenant nous avons le contrôle sur la façon dont nous voulons que Java compare nos objets d'argent. Lançons son test équivalent:

@Test
public void givenTwoSameValueMoneyValueObjects_whenEqualityTestPasses_thenCorrect() {
    ImmutableMoney m1 = new ImmutableMoney(10000, "USD");
    ImmutableMoney m2 = new ImmutableMoney(10000, "USD");
    assertTrue(m1.equals(m2));
}

Notez la sémantique de ce test, nous nous attendons à ce qu'il réussisse lorsque les deux objets monétaires testent égaux via la méthodeequals.

4. Pourquoi AutoValue?

Maintenant que nous comprenons parfaitement les types de valeur et pourquoi nous en avons besoin, nous pouvons examiner AutoValue et son intégration dans l'équation.

4.1. Problèmes avec le codage manuel

Lorsque nous créons des types de valeur comme nous l'avons fait dans la section précédente, nous allons rencontrer un certain nombre de problèmes liés àbad design and a lot of boilerplate code.

Une classe à deux champs aura 9 lignes de code: une pour la déclaration du paquet, deux pour la signature de la classe et son accolade de fermeture, deux pour les déclarations de champ, deux pour les constructeurs et son accolade de fermeture et deux pour l'initialisation des champs, mais nous avons besoin de getters pour les champs, chacun prenant trois lignes de code supplémentaires, soit six lignes supplémentaires.

Le remplacement des méthodeshashCode() etequalTo(Object) nécessite respectivement environ 9 lignes et 18 lignes et le remplacement de la méthodetoString() ajoute cinq lignes supplémentaires.

Cela signifie qu'une base de code bien formatée pour nos deux classes de champs prendraitabout 50 lines of code.

4.2 IDEs to The Rescue?

C'est facile avec un IDE comme Eclipse ou IntilliJ et avec seulement une ou deux classes à valeur typée à créer. Pensez à une multitude de classes à créer, serait-ce toujours aussi facile même si l'IDE nous aide?

Avance rapide, quelques mois plus tard, supposons que nous devions revoir notre code et apporter des modifications à nos classesMoney et peut-être convertir le champcurrency du typeString en un autre type valeur appeléCurrency.

4.3 IDEs Not Really so Helpful

Un IDE comme Eclipse ne peut pas simplement modifier pour nous nos méthodes d’accesseurs ni les méthodestoString(),hashCode() ouequals(Object).

This refactoring would have to be done by hand. L'édition de code augmente le potentiel de bogues et à chaque nouveau champ que nous ajoutons à la classeMoney, le nombre de lignes augmente de façon exponentielle.

Reconnaître le fait que ce scénario se produit, qu'il se produit souvent et en grande quantité nous fera vraiment apprécier le rôle d'AutoValue.

5. Exemple d'AutoValue

Le problème résolu par AutoValue est de supprimer tout le code standard dont nous avons parlé dans la section précédente, de sorte que nous n'ayons jamais à l'écrire, le modifier ni même le lire.

Nous allons regarder le même exemple deMoney, mais cette fois avec AutoValue. Nous appellerons cette classeAutoValueMoney par souci de cohérence:

@AutoValue
public abstract class AutoValueMoney {
    public abstract String getCurrency();
    public abstract long getAmount();

    public static AutoValueMoney create(String currency, long amount) {
        return new AutoValue_AutoValueMoney(currency, amount);
    }
}

Ce qui s'est passé, c'est que nous écrivons une classe abstraite, définissons des accesseurs abstraits pour elle mais pas de champs, nous annotons la classe avec@AutoValue totalisant tous seulement 8 lignes de code, etjavac génère une sous-classe concrète pour nous qui ressemble à ceci:

public final class AutoValue_AutoValueMoney extends AutoValueMoney {
    private final String currency;
    private final long amount;

    AutoValue_AutoValueMoney(String currency, long amount) {
        if (currency == null) throw new NullPointerException(currency);
        this.currency = currency;
        this.amount = amount;
    }

    // standard getters

    @Override
    public int hashCode() {
        int h = 1;
        h *= 1000003;
        h ^= currency.hashCode();
        h *= 1000003;
        h ^= amount;
        return h;
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }
        if (o instanceof AutoValueMoney) {
            AutoValueMoney that = (AutoValueMoney) o;
            return (this.currency.equals(that.getCurrency()))
              && (this.amount == that.getAmount());
        }
        return false;
    }
}

Nous n'avons jamais du tout à traiter directement cette classe, ni à la modifier lorsque nous devons ajouter plus de champs ou apporter des modifications à nos champs comme le scénariocurrency dans la section précédente.

Javac will always regenerate updated code for us.

Lors de l'utilisation de ce nouveau type de valeur, tous les appelants ne voient que le type parent, comme nous le verrons dans les tests unitaires suivants.

Voici un test qui vérifie que nos champs sont correctement définis:

@Test
public void givenValueTypeWithAutoValue_whenFieldsCorrectlySet_thenCorrect() {
    AutoValueMoney m = AutoValueMoney.create("USD", 10000);
    assertEquals(m.getAmount(), 10000);
    assertEquals(m.getCurrency(), "USD");
}

Un test pour vérifier que deux objetsAutoValueMoney avec la même devise et le même test de montant sont égaux suivent:

@Test
public void given2EqualValueTypesWithAutoValue_whenEqual_thenCorrect() {
    AutoValueMoney m1 = AutoValueMoney.create("USD", 5000);
    AutoValueMoney m2 = AutoValueMoney.create("USD", 5000);
    assertTrue(m1.equals(m2));
}

Lorsque nous changeons le type de devise d'un objet monétaire en GBP, le test:5000 GBP == 5000 USD n'est plus vrai:

@Test
public void given2DifferentValueTypesWithAutoValue_whenNotEqual_thenCorrect() {
    AutoValueMoney m1 = AutoValueMoney.create("GBP", 5000);
    AutoValueMoney m2 = AutoValueMoney.create("USD", 5000);
    assertFalse(m1.equals(m2));
}

6. AutoValue avec les constructeurs

L'exemple initial que nous avons examiné couvre l'utilisation de base d'AutoValue à l'aide d'une méthode d'usine statique en tant qu'API de création publique.

Notice that if all our fields were*Strings*, il serait facile de les échanger car nous les avons passés à la méthode de fabrique statique, comme placer lesamount à la place decurrency et vice versa.

Ceci est particulièrement susceptible de se produire si nous avons de nombreux champs et tous sont de typeString. Ce problème est aggravé par le fait qu'avec AutoValue,all fields are initialized through the constructor.

Pour résoudre ce problème, nous devons utiliser le modèlebuilder. Heureusement. Cela peut être généré par AutoValue.

Notre classe AutoValue ne change pas beaucoup, sauf que la méthode de fabrique statique est remplacée par un générateur:

@AutoValue
public abstract class AutoValueMoneyWithBuilder {
    public abstract String getCurrency();
    public abstract long getAmount();
    static Builder builder() {
        return new AutoValue_AutoValueMoneyWithBuilder.Builder();
    }

    @AutoValue.Builder
    abstract static class Builder {
        abstract Builder setCurrency(String currency);
        abstract Builder setAmount(long amount);
        abstract AutoValueMoneyWithBuilder build();
    }
}

La classe générée est identique à la première mais une classe interne concrète pour le générateur est générée et implémente les méthodes abstraites dans le générateur:

static final class Builder extends AutoValueMoneyWithBuilder.Builder {
    private String currency;
    private long amount;
    Builder() {
    }
    Builder(AutoValueMoneyWithBuilder source) {
        this.currency = source.getCurrency();
        this.amount = source.getAmount();
    }

    @Override
    public AutoValueMoneyWithBuilder.Builder setCurrency(String currency) {
        this.currency = currency;
        return this;
    }

    @Override
    public AutoValueMoneyWithBuilder.Builder setAmount(long amount) {
        this.amount = amount;
        return this;
    }

    @Override
    public AutoValueMoneyWithBuilder build() {
        String missing = "";
        if (currency == null) {
            missing += " currency";
        }
        if (amount == 0) {
            missing += " amount";
        }
        if (!missing.isEmpty()) {
            throw new IllegalStateException("Missing required properties:" + missing);
        }
        return new AutoValue_AutoValueMoneyWithBuilder(this.currency,this.amount);
    }
}

Remarquez également que les résultats des tests ne changent pas.

Si nous voulons savoir que les valeurs de champ sont en réalité correctement définies via le générateur, nous pouvons exécuter ce test:

@Test
public void givenValueTypeWithBuilder_whenFieldsCorrectlySet_thenCorrect() {
    AutoValueMoneyWithBuilder m = AutoValueMoneyWithBuilder.builder().
      setAmount(5000).setCurrency("USD").build();
    assertEquals(m.getAmount(), 5000);
    assertEquals(m.getCurrency(), "USD");
}

Pour tester cette égalité dépend de l'état interne:

@Test
public void given2EqualValueTypesWithBuilder_whenEqual_thenCorrect() {
    AutoValueMoneyWithBuilder m1 = AutoValueMoneyWithBuilder.builder()
      .setAmount(5000).setCurrency("USD").build();
    AutoValueMoneyWithBuilder m2 = AutoValueMoneyWithBuilder.builder()
      .setAmount(5000).setCurrency("USD").build();
    assertTrue(m1.equals(m2));
}

Et lorsque les valeurs de champ sont différentes:

@Test
public void given2DifferentValueTypesBuilder_whenNotEqual_thenCorrect() {
    AutoValueMoneyWithBuilder m1 = AutoValueMoneyWithBuilder.builder()
      .setAmount(5000).setCurrency("USD").build();
    AutoValueMoneyWithBuilder m2 = AutoValueMoneyWithBuilder.builder()
      .setAmount(5000).setCurrency("GBP").build();
    assertFalse(m1.equals(m2));
}

7. Conclusion

Dans ce didacticiel, nous avons présenté la plupart des bases de la bibliothèque AutoValue de Google et comment l'utiliser pour créer des types de valeur avec très peu de code de notre part.

Une alternative à l'AutoValue de Google est leLombok project - vous pouvez consulter l'article d'introduction sur l'utilisation deLombok here.

L'implémentation complète de tous ces exemples et extraits de code peut être trouvée dans AutoValueGitHub project.