Введение в AutoValue

Введение в AutoValue

1. обзор

AutoValue - это генератор исходного кода для Java, а точнее, библиотека дляgenerating source code for value objects or value-typed objects.

Чтобы сгенерировать объект-значение, все, что вам нужно сделать, это выполнитьannotate an abstract class with the @AutoValue annotation и скомпилировать свой класс. Генерируется объект значения с методами доступа, параметризованным конструктором, правильно переопределенными методамиtoString(), equals(Object) иhashCode().

Следующий фрагмент кода представляет собойa quick example абстрактного класса, который при компиляции приведет к объекту значения с именем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();
}

Давайте продолжим и узнаем больше об объектах-значениях, зачем они нам и как AutoValue может помочь сделать задачу генерации и рефакторинга кода намного менее затратной по времени.

2. Maven Setup

Чтобы использовать AutoValue в проектах Maven, вам необходимо включить следующую зависимость вpom.xml:


    com.google.auto.value
    auto-value
    1.2

Последнюю версию можно найти, набравthis link.

3. Объекты типа значения

Типы-значения являются конечным продуктом библиотеки, поэтому, чтобы оценить ее место в наших задачах по разработке, мы должны тщательно понимать типы-значения, то, чем они являются, чем они не являются и зачем они нам нужны.

3.1. Что такое типы значений?

Объекты типа значения - это объекты, равенство которых друг другу определяется не идентичностью, а их внутренним состоянием. Это означает, что два экземпляра объекта с типом значения считаются равными, если они имеют равные значения поля.

Typically, value-types are immutable. Их поля должны иметь видfinal, и они не должны иметь методовsetter, так как это сделает их изменяемыми после создания экземпляра.

Они должны потреблять все значения полей через конструктор или фабричный метод.

Типы значений не являются компонентами JavaBeans, потому что у них нет конструктора аргументов по умолчанию или нулевого аргумента, а также у них нет методов установки, аналогичноthey are not Data Transfer Objects nor Plain Old Java Objects.

Кроме того, тип с типом значения должен быть конечным, чтобы он не расширялся, хотя бы кто-то переопределял методы. JavaBeans, DTO и POJO не обязательно должны быть окончательными.

3.2. Создание типа значения

Предположим, мы хотим создать тип-значение с именемFoo с полями с именамиtext иnumber.. Как бы мы это сделали?

Мы сделаем финальный класс и отметим все его поля как финальные. Затем мы использовали бы среду IDE для создания конструктора, методаhashCode(), методаequals(Object),getters в качестве обязательных методов и методаtoString(), и у нас был бы класс так:

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;
    }
}

После создания экземпляраFoo мы ожидаем, что его внутреннее состояние останется неизменным на протяжении всего жизненного цикла.

Как мы увидим в следующем подразделеthe hashCode of an object must change from instance to instance, но для типов значений мы должны привязать его к полям, которые определяют внутреннее состояние объекта значения.

Следовательно, даже изменение поля того же объекта приведет к изменению значенияhashCode.

3.3. Как работают типы значений

Причина того, что значения-типы должны быть неизменными, состоит в том, чтобы предотвратить любые изменения их внутреннего состояния приложением после того, как они были созданы.

Всякий раз, когда мы хотим сравнить любые два объекта с типом значения,we must, therefore, use the equals(Object) method of the Object class.

Это означает, что мы всегда должны переопределять этот метод в наших собственных типах значений и возвращать true только в том случае, если поля сравниваемых объектов значений имеют одинаковые значения.

Более того, чтобы мы могли без нарушения использовать наши объекты значений в коллекциях на основе хешей, таких какHashSets иHashMaps,we must properly implement the hashCode() method.

3.4. Зачем нужны типы ценностей

Потребность в типах значений возникает довольно часто. Это случаи, когда мы хотели бы переопределить поведение по умолчанию исходного классаObject.

Как мы уже знаем, реализация классаObject по умолчанию считает два объекта равными, если они имеют одинаковую идентичность, однакоfor our purposes we consider two objects equal when they have the same internal state.

Предполагая, что мы хотели бы создать денежный объект следующим образом:

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

}

Мы можем запустить следующий тест для проверки его равенства:

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

Обратите внимание на семантику теста.

Мы считаем, что прошло, когда два денежных объекта не равны. Это потому, чтоwe have not overridden the equals method, поэтому равенство измеряется путем сравнения ссылок на память объектов, которые, конечно, не будут разными, потому что это разные объекты, занимающие разные места в памяти.

Каждый объект составляет 10 000 долларов США, ноJava tells us our money objects are not equal. Мы хотим, чтобы два объекта тестировали неравно, только когда суммы в валюте разные или типы валют разные.

Теперь давайте создадим объект эквивалентного значения, и на этот раз мы позволим IDE генерировать большую часть кода:

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;
    }
}

Единственная разница в том, что мы переопределили методыequals(Object) иhashCode(), теперь у нас есть контроль над тем, как мы хотим, чтобы Java сравнивала наши денежные объекты. Давайте запустим эквивалентный тест:

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

Обратите внимание на семантику этого теста: мы ожидаем, что он пройдет, когда оба объекта денег проверяются на равенство с помощью методаequals.

4. Почему AutoValue?

Теперь, когда мы полностью понимаем типы значений и зачем они нам нужны, мы можем взглянуть на AutoValue и узнать, как оно входит в уравнение.

4.1. Проблемы с ручным кодированием

Когда мы создаем типы значений, как мы это делали в предыдущем разделе, мы столкнемся с рядом проблем, связанных сbad design and a lot of boilerplate code.

У класса с двумя полями будет 9 строк кода: одна для объявления пакета, две для сигнатуры класса и его закрывающей скобки, две для объявлений полей, две для конструкторов и закрывающей скобки и две для инициализации полей, но тогда нам нужны геттеры для полей, каждая из которых занимает еще три строки кода, делая шесть дополнительных строк.

Для переопределения методовhashCode() иequalTo(Object) требуется около 9 строк и 18 строк соответственно, а переопределение методаtoString() добавляет еще пять строк.

Это означает, что хорошо отформатированная кодовая база для наших двух классов полей займетabout 50 lines of code.

4.2 IDEs to The Rescue?

Это легко сделать с помощью IDE, такой как Eclipse или IntilliJ, и создать только один или два класса с типами значений. Подумайте о том, чтобы создать множество таких классов. Будет ли это так же просто, даже если IDE поможет нам?

Перенесемся вперед, через несколько месяцев, предположим, что нам нужно пересмотреть наш код и внести поправки в наши классыMoney и, возможно, преобразовать полеcurrency из типаString в другой тип значения называетсяCurrency.

4.3 IDEs Not Really so Helpful

IDE, такая как Eclipse, не может просто редактировать для нас ни наши методы доступа, ни методыtoString(),hashCode() илиequals(Object).

This refactoring would have to be done by hand. Редактирование кода увеличивает вероятность ошибок, и с каждым новым полем, которое мы добавляем в классMoney, количество строк увеличивается экспоненциально.

Признание того факта, что этот сценарий происходит, что это происходит часто и в больших объемах, заставит нас по-настоящему оценить роль AutoValue.

5. Пример AutoValue

Проблема, которую решает AutoValue, заключается в том, чтобы убрать весь стандартный код, о котором мы говорили в предыдущем разделе, так, чтобы нам никогда не приходилось его писать, редактировать или даже читать.

Мы рассмотрим тот же примерMoney, но на этот раз с AutoValue. Мы будем называть этот классAutoValueMoney для согласованности:

@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);
    }
}

Произошло то, что мы пишем абстрактный класс, определяем для него абстрактные методы доступа, но не поля, мы аннотируем класс с помощью@AutoValue, всего всего 8 строк кода, аjavac генерирует конкретный подкласс для us, который выглядит так:

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;
    }
}

Нам вообще никогда не нужно иметь дело с этим классом напрямую, и нам не нужно его редактировать, когда нам нужно добавить дополнительные поля или внести изменения в наши поля, как в сценарииcurrency в предыдущем разделе.

Javac will always regenerate updated code for us.

При использовании этого нового типа значения все вызывающие абоненты видят только родительский тип, как мы увидим в следующих тестах модулей.

Вот тест, который проверяет, что наши поля установлены правильно:

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

Тест для проверки того, что два объектаAutoValueMoney с одинаковой валютой и одинаковой суммой равны:

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

Когда мы меняем тип валюты одного денежного объекта на GBP, проверка:5000 GBP == 5000 USD больше не верна:

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

6. AutoValue со строителями

Начальный пример, который мы рассмотрели, охватывает базовое использование AutoValue с использованием статического метода фабрики в качестве нашего публичного API создания.

Notice that if all our fields were*Strings*,, их будет легко поменять местами, поскольку мы передали их статическому методу factory, например, поместивamount вместоcurrency и наоборот.

Это особенно вероятно, если у нас много полей, и все они имеют типString. Проблема усугубляется тем, что с AutoValueall fields are initialized through the constructor.

Чтобы решить эту проблему, мы должны использовать шаблонbuilder. К счастью. это может быть сгенерировано AutoValue.

Наш класс AutoValue мало что меняет, за исключением того, что статический метод фабрики заменяется компоновщиком:

@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();
    }
}

Сгенерированный класс такой же, как первый, но генерируется конкретный внутренний класс для компоновщика, а также реализуются абстрактные методы в компоновщике:

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);
    }
}

Обратите также внимание на то, что результаты теста не меняются.

Если мы хотим знать, что значения полей на самом деле правильно установлены через конструктор, мы можем выполнить этот тест:

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

Чтобы проверить это равенство зависит от внутреннего состояния:

@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));
}

И когда значения полей разные:

@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. Заключение

В этом руководстве мы познакомили с большинством основ библиотеки Google AutoValue и познакомили с ее использованием для создания типов значений с очень небольшим количеством кода с нашей стороны.

Альтернативой Google AutoValue являетсяLombok project - вы можете прочитать вводную статью об использованииLombok here.

Полную реализацию всех этих примеров и фрагментов кода можно найти в AutoValueGitHub project.