Introdução ao AutoValue
1. Visão geral
AutoValue é um gerador de código-fonte para Java e, mais especificamente, é uma biblioteca paragenerating source code for value objects or value-typed objects.
Para gerar um objeto do tipo valor, tudo que você precisa fazer éannotate an abstract class with the @AutoValue annotatione compilar sua classe. O que é gerado é um objeto de valor com métodos acessadores, construtor parametrizado, métodostoString(), equals(Object)ehashCode() devidamente substituídos.
O fragmento de código a seguir éa quick example de uma classe abstrata que, quando compilada, resultará em um objeto de valor denominadoAutoValue_Person.
@AutoValue
abstract class Person {
static Person create(String name, int age) {
return new AutoValue_Person(name, age);
}
abstract String name();
abstract int age();
}
Vamos continuar e descobrir mais sobre objetos de valor, por que precisamos deles e como o AutoValue pode ajudar a tornar a tarefa de geração e refatoração de código muito menos demorada.
2. Configuração do Maven
Para usar o AutoValue em projetos Maven, você precisa incluir a seguinte dependência nopom.xml:
com.google.auto.value
auto-value
1.2
A versão mais recente pode ser encontrada seguindothis link.
3. Objetos com valor digitado
Tipos de valor são o produto final da biblioteca; portanto, para apreciar seu lugar em nossas tarefas de desenvolvimento, precisamos entender completamente os tipos de valor, o que são, o que não são e por que precisamos deles.
3.1. O que são tipos de valor?
Objetos do tipo valor são objetos cuja igualdade entre si não é determinada pela identidade, mas pelo seu estado interno. Isso significa que duas instâncias de um objeto digitado por valor são consideradas iguais desde que tenham valores de campo iguais.
Typically, value-types are immutable. Seus campos devem serfinal e eles não devem ter métodossetter, pois isso os tornará alteráveis após a instanciação.
Eles devem consumir todos os valores de campo por meio de um método construtor ou de fábrica.
Os tipos de valor não são JavaBeans porque não têm um construtor de argumento padrão ou zero e nem têm métodos setter, da mesma forma,they are not Data Transfer Objects nor Plain Old Java Objects.
Além disso, uma classe de valor digitado deve ser final, para que não sejam extensíveis, pelo menos que alguém substitua os métodos. JavaBeans, DTOs e POJOs não precisam ser finais.
3.2. Criação de um tipo de valor
Supondo que queremos criar um tipo de valor chamadoFoo com campos chamadostextenumber., como faríamos isso?
Faríamos uma aula final e marcaríamos todos os seus campos como finais. Então, usaríamos o IDE para gerar o construtor, o métodohashCode(), o métodoequals(Object), ogetters como métodos obrigatórios e um métodotoString(), e teríamos um classe assim:
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;
}
}
Depois de criar uma instância deFoo, esperamos que seu estado interno permaneça o mesmo durante todo o seu ciclo de vida.
Como veremos na subseção seguintethe hashCode of an object must change from instance to instance, mas para tipos de valor, temos que amarrá-lo aos campos que definem o estado interno do objeto de valor.
Portanto, mesmo alterar um campo do mesmo objeto alteraria o valor dehashCode.
3.3. Como funcionam os tipos de valor
A razão pela qual os tipos de valor devem ser imutáveis é impedir qualquer alteração em seu estado interno pelo aplicativo após a instanciação.
Sempre que quisermos comparar quaisquer dois objetos com tipo de valor,we must, therefore, use the equals(Object) method of the Object class.
Isso significa que devemos sempre substituir esse método em nossos próprios tipos de valor e retornar somente true se os campos dos objetos de valor que estamos comparando tiverem valores iguais.
Além disso, para usarmos nossos objetos de valor em coleções baseadas em hash comoHashSets eHashMaps sem quebrar,we must properly implement the hashCode() method.
3.4. Por que precisamos de tipos de valor
A necessidade de tipos de valor surge com bastante frequência. Esses são os casos em que gostaríamos de substituir o comportamento padrão da classeObject original.
Como já sabemos, a implementação padrão da classeObject considera dois objetos iguais quando eles têm a mesma identidade, porémfor our purposes we consider two objects equal when they have the same internal state.
Supondo que gostaríamos de criar um objeto money da seguinte maneira:
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
}
Podemos executar o seguinte teste nele para testar sua igualdade:
@Test
public void givenTwoSameValueMoneyObjects_whenEqualityTestFails_thenCorrect() {
MutableMoney m1 = new MutableMoney(10000, "USD");
MutableMoney m2 = new MutableMoney(10000, "USD");
assertFalse(m1.equals(m2));
}
Observe a semântica do teste.
Consideramos que passou quando os dois objetos monetários não são iguais. Isso ocorre porquewe have not overridden the equals method assim a igualdade é medida comparando as referências de memória dos objetos, que obviamente não serão diferentes porque são objetos diferentes ocupando posições de memória diferentes.
Cada objeto representa 10.000 USD, masJava tells us our money objects are not equal. Queremos que os dois objetos testem desiguais apenas quando os valores da moeda forem diferentes ou os tipos de moeda forem diferentes.
Agora vamos criar um objeto de valor equivalente e, desta vez, permitiremos que o IDE gere a maior parte do código:
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;
}
}
A única diferença é que substituímos os métodosequals(Object)ehashCode(), agora temos controle sobre como queremos que o Java compare nossos objetos de dinheiro. Vamos fazer seu teste equivalente:
@Test
public void givenTwoSameValueMoneyValueObjects_whenEqualityTestPasses_thenCorrect() {
ImmutableMoney m1 = new ImmutableMoney(10000, "USD");
ImmutableMoney m2 = new ImmutableMoney(10000, "USD");
assertTrue(m1.equals(m2));
}
Observe a semântica deste teste, esperamos que ele passe quando ambos os objetos de dinheiro forem testados iguais por meio do métodoequals.
4. Por que AutoValue?
Agora que entendemos completamente os tipos de valor e por que precisamos deles, podemos examinar o AutoValue e como ele entra na equação.
4.1. Problemas com codificação manual
Quando criamos tipos de valor como fizemos na seção anterior, encontraremos vários problemas relacionados abad design and a lot of boilerplate code.
Uma classe de dois campos terá 9 linhas de código: uma para declaração de pacote, duas para a assinatura da classe e sua chave de fechamento, duas para declarações de campo, duas para construtores e sua chave de fechamento e duas para inicializar os campos, mas precisamos de getters para os campos, cada um usando mais três linhas de código, criando seis linhas extras.
Substituir os métodoshashCode()eequalTo(Object) requer cerca de 9 linhas e 18 linhas, respectivamente, e substituir o métodotoString() adiciona outras cinco linhas.
Isso significa que uma base de código bem formatada para nossa classe de dois campos levariaabout 50 lines of code.
4.2 IDEs to The Rescue?
Isso é fácil com um IDE como Eclipse ou IntilliJ e com apenas uma ou duas classes de tipo de valor para criar. Pense em várias dessas classes para criar, ainda assim seria tão fácil, mesmo que o IDE nos ajude?
Avance rapidamente, alguns meses depois, suponha que temos que revisitar nosso código e fazer alterações em nossas classesMoney e talvez converter o campocurrency do tipoString para outro tipo de valor chamadoCurrency.
4.3 IDEs Not Really so Helpful
Um IDE como o Eclipse não pode simplesmente editar para nós nossos métodos de acesso nem os métodostoString(),hashCode() ouequals(Object).
This refactoring would have to be done by hand. A edição do código aumenta o potencial de bugs e, a cada novo campo que adicionamos à classeMoney, o número de linhas aumenta exponencialmente.
Reconhecer o fato de que esse cenário acontece, que acontece com frequência e em grandes volumes nos fará realmente apreciar o papel do AutoValue.
5. Exemplo de AutoValue
O problema que o AutoValue resolve é retirar todo o código padrão que falamos na seção anterior, para que nunca tenhamos que escrevê-lo, editá-lo ou mesmo lê-lo.
Veremos o mesmo exemplo deMoney, mas desta vez com AutoValue. Chamaremos essa classe deAutoValueMoney por uma questão de consistência:
@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);
}
}
O que aconteceu é que escrevemos uma classe abstrata, definimos acessadores abstratos para ela, mas sem campos, anotamos a classe com@AutoValue totalizando apenas 8 linhas de código ejavac gera uma subclasse concreta para nós que se parece com isto:
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;
}
}
Nunca precisamos lidar com essa classe diretamente, nem precisamos editá-la quando precisamos adicionar mais campos ou fazer alterações em nossos campos, como o cenáriocurrency na seção anterior.
Javac will always regenerate updated code for us.
Ao usar esse novo tipo de valor, todos os chamadores vêem apenas o tipo pai, como veremos nos seguintes testes de unidade.
Aqui está um teste que verifica se nossos campos estão sendo definidos corretamente:
@Test
public void givenValueTypeWithAutoValue_whenFieldsCorrectlySet_thenCorrect() {
AutoValueMoney m = AutoValueMoney.create("USD", 10000);
assertEquals(m.getAmount(), 10000);
assertEquals(m.getCurrency(), "USD");
}
Um teste para verificar se dois objetosAutoValueMoney com a mesma moeda e o mesmo valor são iguais seguem:
@Test
public void given2EqualValueTypesWithAutoValue_whenEqual_thenCorrect() {
AutoValueMoney m1 = AutoValueMoney.create("USD", 5000);
AutoValueMoney m2 = AutoValueMoney.create("USD", 5000);
assertTrue(m1.equals(m2));
}
Quando alteramos o tipo de moeda de um objeto de dinheiro para GBP, o teste:5000 GBP == 5000 USD não é mais verdadeiro:
@Test
public void given2DifferentValueTypesWithAutoValue_whenNotEqual_thenCorrect() {
AutoValueMoney m1 = AutoValueMoney.create("GBP", 5000);
AutoValueMoney m2 = AutoValueMoney.create("USD", 5000);
assertFalse(m1.equals(m2));
}
6. AutoValue With Builders
O exemplo inicial que examinamos abrange o uso básico do AutoValue usando um método estático de fábrica como nossa API de criação pública.
Notice that if all our fields were*Strings*, seria fácil intercambiá-los conforme os passamos para o método de fábrica estático, como colocaramount no lugar decurrencye vice-versa.
Isso é mais provável de acontecer se tivermos muitos campos e todos forem do tipoString. Este problema é agravado pelo fato de que com AutoValue,all fields are initialized through the constructor.
Para resolver este problema, devemos usar o padrãobuilder. Felizmente. isso pode ser gerado pelo AutoValue.
Nossa classe AutoValue realmente não muda muito, exceto que o método estático de fábrica é substituído por um construtor:
@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();
}
}
A classe gerada é igual à primeira, mas é gerada uma classe interna concreta para o construtor, implementando os métodos abstratos no construtor:
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);
}
}
Observe também como os resultados do teste não mudam.
Se quisermos saber que os valores dos campos são realmente definidos corretamente pelo construtor, podemos executar este teste:
@Test
public void givenValueTypeWithBuilder_whenFieldsCorrectlySet_thenCorrect() {
AutoValueMoneyWithBuilder m = AutoValueMoneyWithBuilder.builder().
setAmount(5000).setCurrency("USD").build();
assertEquals(m.getAmount(), 5000);
assertEquals(m.getCurrency(), "USD");
}
Para testar se a igualdade depende do estado interno:
@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));
}
E quando os valores do campo são diferentes:
@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. Conclusão
Neste tutorial, apresentamos a maioria dos fundamentos da biblioteca AutoValue do Google e como usá-la para criar tipos de valor com muito pouco código de nossa parte.
Uma alternativa ao AutoValue do Google é oLombok project - você pode dar uma olhada no artigo introdutório sobre como usarLombok here.
A implementação completa de todos esses exemplos e trechos de código pode ser encontrada em AutoValueGitHub project.