AutoValue

AutoValue

AutoValue to rozwijana przez Google na zasadach wolnej licencji (Apache 2.0) biblioteka pozwalająca łatwiej i przyjemniej obchodzić się z klasami typu value object. Dzięki niej w bardzo prosty sposób można uniknąć czasochłonnego i błędogennego pisania oraz utrzymywania metod equals, hashCode oraz toString.

Poniższy kod przedstawia prostą klasę napisaną z wykorzystaniem AutoValue.

@AutoValue
public abstract class UserWithFactoryMethod {

	static UserWithFactoryMethod create(String name, int age) {
		return new AutoValue_UserWithFactoryMethod(name, age);
	}

	abstract String getName();
	abstract int getAge();
}
UserWithFactoryMethod userWithFactoryMethod
= UserWithFactoryMethod.create("Tomasz", 100);

W celu wygenerowania w pełni funkcjonalnej klasy value object, wystarczy zdefiniować abstrakcyjną klasę (interfejsy nie są wspierane) i dodać dla niej adnotację @AutoValue oraz statyczną metodę fabryczną, która utworzy nowy obiekt. Całą resztą zajmie się AutoValue.

Metoda fabryczna create (nazwa jest dowolna, jednak ze względu na konwencję nazewnictwa zalecam zostawić create) tworzy nowy obiekt klasy AutoValue_User, która zostanie wygenerowana przez bibliotekę podczas kompilacji. Nazwa nowej klasy to przedrostek AutoValue_ oraz nazwa naszej klasy.

package pl.stormit.autovalue;

import javax.annotation.Generated;

@Generated("com.google.auto.value.processor.AutoValueProcessor")
final class AutoValue_UserWithFactoryMethod extends UserWithFactoryMethod {

	private final String name;
	private final int age;

	AutoValue_UserWithFactoryMethod(
			String name,
			int age) {
		if (name == null) {
			throw new NullPointerException("Null name");
		}
		this.name = name;
		this.age = age;
	}

	@Override
	String getName() {
		return name;
	}

	@Override
	int getAge() {
		return age;
	}

	@Override
	public String toString() {
		return "UserWithFactoryMethod{"
				+ "name=" + name + ", "
				+ "age=" + age
				+ "}";
	}

	@Override
	public boolean equals(Object o) {
		if (o == this) {
			return true;
		}
		if (o instanceof UserWithFactoryMethod) {
			UserWithFactoryMethod that = (UserWithFactoryMethod) o;
			return (this.name.equals(that.getName())) && (this.age == that.getAge());
		}
		return false;
	}

	@Override
	public int hashCode() {
		int h = 1;
		h *= 1000003;
		h ^= this.name.hashCode();
		h *= 1000003;
		h ^= this.age;
		return h;
	}
}

AutoValue_UserWithFactoryMethod jest to pełnoprawna klasa, której kod można podejrzeć i np. debugować. Nie powinno się jednak jej modyfikować, ponieważ zmiany zostaną nadpisane przy najbliższej kompilacji.

Uwagi i spostrzeżenia na temat wygenerowanego kodu:

  • powstała klasa dziedziczy po napisanej przez nas ręcznie. Jeżeli dodatkowo wszystkie odwołania w kodzie będą odnosić się do klasy bazowej, reszta aplikacji może być nieświadoma istnienia klas wygenerowanych przez bibliotekę
  • AutoValue samo utworzyło pola klasy na podstawie abstrakcyjnych getterów
  • ponieważ wszystkie obiekty zarządzane przez bibliotekę są z założenia niemodyfikowalne (immutable), w powstałym kodzie nie ma setterów. Nie można również dodać ich w klasie bazowej, ponieważ utworzone pola są oznaczone jako final.
  • klasa ma wygenerowane metody equals, hashCode oraz toString uwzględniające wszystkie pola
  • adnotacje z abstrakcyjnych getterów są przekopiowane do ich implementacji. Nie ma natomiast możliwości adnotowania utworzonych pól.

Przyczyna powstania biblioteki

Głównym powodem jest chęć automatyzacji wytwarzania oraz utrzymywania powtarzalnego kodu. Więcej na ten temat można przeczytać w artykule o boilerplate code, w tekście przedstawione są również alternatywne rozwiązania dla AutoValue.

AutoValue - wprowadzenie

AutoValue – wprowadzenie

Wprowadzenie do technologii

AutoValue działa jako standardowy procesor adnotacji w obrębie kompilatora javac. Nowy kod powstaje w obrębie tego samego pakietu na podstawie abstrakcyjnej klasy pisanej ręcznie przez programistę.

W celu nadpisania standardowej implementacji biblioteki dla metod hashCode, equals i toString wystarczy je zadeklarować w klasie abstrakcyjnej. Kompilator sam rozpozna, żeby nie generować dla nich implementacji w klasie dziedziczącej AutoValue. Dobrym zwyczajem, aczkolwiek niekoniecznym, jest również oznaczenie tych metod jako final, żeby wyraźnie zaznaczyć, że framework nie modyfikuje już ich zachowania.

Czasami zachodzi potrzeba wykluczenia jakiegoś pola z automatycznie generowanych metod, w tym celu wystarczy nie deklarować abstrakcyjnego gettera, a to pole zadeklarować ręcznie w swojej klasie. Dla AutoValue takie pole będzie niewidoczne.

Generowanie obiektów z wykorzystaniem buildera

Poza podstawowym sposobem generowania obiektów przez factory method, biblioteka oferuje również możliwość tworzenia nowych obiektów z wykorzystaniem wzorca projektowego builder. To rozwiązanie jest bardziej wyrafinowane i daje więcej możliwości. Poniżej zmodyfikowana klasa użytkownika, z podstawową implementacją buildera.

@AutoValue
public abstract class UserWithBuilder {

	abstract String getName();

	abstract int getAge();

	static Builder builder() {
		return new AutoValue_UserWithBuilder.Builder();
	}

	@AutoValue.Builder
	abstract static class Builder {

		abstract Builder setName(String value);

		abstract Builder setAge(int value);

		abstract UserWithBuilder build();
	}
}

 

Biblioteka narzuca wybranie jednego z dwóch sposobów generowania obiektów. Wybór buildera automatycznie generuje prywatny konstruktor w generowanej klasie, tym samym blokując możliwość skorzystania z factory method. W tej sytuacji, jeżeli zajdzie taka konieczność, w metodzie create można skorzystać z nowo utworzonego buildera zamiast konstruktora, w celu ominięcia tego ograniczenia.

UserWithBuilder userWithBuilder
= UserWithBuilder.builder().setName("Tomasz").setAge(100).build();

Deklaracja buildera powinna mieć adnotację @AutoValue.Builder oraz abstrakcyjną metodę zwracającą główny obiekt. Sama nazwa tej metody jest dowolna, jednak zaleca się wykorzystanie nazwy build.

Natomiast klasa buildera może zostać zadeklarowana poza główną klasą, jednak tu również zaleca się jej deklarację jako wewnętrzną statyczną klasę, w celu zachowania przyjętej konwencji.

Na podstawie tych informacji oraz abstrakcyjnych setterów kompilator powinien wygenerować pełną implementację buildera.

package pl.stormit.autovalue;

import javax.annotation.Generated;

@Generated("com.google.auto.value.processor.AutoValueProcessor")
final class AutoValue_UserWithBuilder extends UserWithBuilder {

	private final String name;
	private final int age;

	private AutoValue_UserWithBuilder(
			String name,
			int age) {
		this.name = name;
		this.age = age;
	}

	@Override
	String getName() {
		return name;
	}

	@Override
	int getAge() {
		return age;
	}

	@Override
	public String toString() {
		return "UserWithBuilder{"
				+ "name=" + name + ", "
				+ "age=" + age
				+ "}";
	}

	@Override
	public boolean equals(Object o) {
		if (o == this) {
			return true;
		}
		if (o instanceof UserWithBuilder) {
			UserWithBuilder that = (UserWithBuilder) o;
			return (this.name.equals(that.getName())) && (this.age == that.getAge());
		}
		return false;
	}

	@Override
	public int hashCode() {
		int h = 1;
		h *= 1000003;
		h ^= this.name.hashCode();
		h *= 1000003;
		h ^= this.age;
		return h;
	}

	static final class Builder extends UserWithBuilder.Builder {
		private String name;
		private Integer age;
		Builder() {
		}
		Builder(UserWithBuilder source) {
			this.name = source.getName();
			this.age = source.getAge();
		}
		@Override
		public UserWithBuilder.Builder setName(String name) {
			this.name = name;
			return this;
		}
		@Override
		public UserWithBuilder.Builder setAge(int age) {
			this.age = age;
			return this;
		}
		@Override
		public UserWithBuilder build() {
			String missing = "";
			if (name == null) {
				missing += " name";
			}
			if (age == null) {
				missing += " age";
			}
			if (!missing.isEmpty()) {
				throw new IllegalStateException("Missing required properties:" + missing);
			}
			return new AutoValue_UserWithBuilder(
					this.name,
					this.age);
		}
	}

}

Memoized – czyli keszowanie wyniku funkcji

Niektóre wyliczenia w metodach są bardzo skomplikowane lub pobierane z bazy danych, co w porównaniu do innych metod może zajmować sporo czasu. Dlatego w celach optymalizacyjnych wartości zwracane przez takie metody dość często przechowuje się w prywatnym polu i tylko za pierwszym razem są one wyliczane.

Taki kod, żeby był w pełni poprawny, powinien również uwzględniać wyścigi wątków oraz być dobrze testowany. Zazwyczaj jednak jest to pomijane.

W takiej sytuacji przychodzi z pomocą AutoValue. Wystarczy dodać adnotacja @Memoized, a całą resztę powtarzalnej logiki wygeneruje framework.

@Memoized
String longRunningMethod() {
	try {
		Thread.sleep(1000);
	} catch (InterruptedException ex) {
		throw new RuntimeException(ex);
	}

	return "" + System.currentTimeMillis();
}

Ponieważ ta funkcjonalność realizowana jest przez rozszerzenie biblioteki, tym razem framework wygeneruje aż 2 klasy:

  • $AutoValue_MemoizedExample – jest do standardowa klasa AutoValue dziedzicząca po głównej klasie użytkownika i zawierająca implementację metod: equals, hashCode i toString oraz factory method lub builder
  • AutoValue_MemoizedExample – klasa wygenerowana przez rozszerzenie z nadpisaną implementacją longRunningMethod
final class AutoValue_MemoizedExample extends $AutoValue_MemoizedExample {
	private volatile String longRunningMethod;

	AutoValue_MemoizedExample() {
		super();
	}

	@Override
	String longRunningMethod() {
		if (longRunningMethod == null) {
			synchronized (this) {
				if (longRunningMethod == null) {
					longRunningMethod = super.longRunningMethod();
					if (longRunningMethod == null) {
						throw new NullPointerException("longRunningMethod() cannot return null");
					}
				}
			}
		}
		return longRunningMethod;
	}
}

Dobre praktyki korzystanie z AutoValue

  • unikanie typów pozwalających modyfikować wartości obiektów – ponieważ value objects mają być w pełni niemodyfikowalne, oznacza to, że również wszystkie typy w nich przechowywane powinny być niemodyfikowalne. Nie oznacza to jednak, że fabryka lub builder może przyjmować tylko takie typy. Ważne jest, żeby po ich przyjęciu zapewnić im niezmienność, np. korzystając z ImmutableList, czy ImmutableSet z Guavy.
  • utrzymywanie prostych klas bez zewnętrznych zależności – wszystkie klasy tego typu powinny być możliwie jak najprostsze oraz nie posiadać dodatkowych zależności. Jeżeli logika staje się zbyt rozbudowana, to najwyższy czas, żeby przenieść ją do jakiegoś serwisu i odchudzić value object.
  • tylko jedna referencja do wygenerowanego kodu – mimo iż jest to technicznie możliwe, to do wygenerowanego kodu nie powinno prowadzić więcej niż jedno odwołanie w całej aplikacji. Będzie to wywołanie konstruktora w factory method lub metody build w builderze. Dzięki takiemu podejściu ewentualne zmiany będą dużo mniej problematyczne. Można również łatwiej wprowadzić specyficzne przetwarzanie przed lub po utworzeniu takiego obiektu.
  • oznaczenie wszystkich zaimplementowanych metod jako final – dzięki temu, czytając klasę napisaną przez użytkownika, wyraźnie widać, że te metody nie będą już modyfikowane przez framework. Ma to szczególne znaczenie w przypadku metod equals, hashCode i toString, które domyślnie są generowane przez AutoValue.

Więcej na temat dobrych praktyk można przeczytać w artykule na stronie biblioteki.

Dlaczego warto korzystać?

  • AutoValue nie przenika do API, dzięki czemu programista korzystający z utworzonych klas może być całkowicie nieświadomi, że powstały z pomocą tej biblioteki
  • brak zależności w runtime aplikacji
  • pomijalna różnica w wydajności
  • proste i przejrzyste rozwiązanie oparte na standardowych mechanizmach Javy

Instalacja

Do projektu wystarczy dodać jedną zależność, która jest tylko potrzebna podczas kompilacji aplikacji (scope: provided). Podczas działania aplikacja będzie korzystała z wcześniej wygenerowanych klas i nie potrzebuje już żadnych dodatkowych zależności.

Najnowszą wersję biblioteki można pobrać z repozytorium maven.

<dependency>
	<groupId>com.google.auto.value</groupId>
	<artifactId>auto-value</artifactId>
	<version>1.4-rc1</version>
	<scope>provided</scope>
</dependency>

Wnioski

Dzięki AutoValue można skupić się na logice biznesowej, zostawiając nudną i potencjalnie błędogenną implementację niskopoziomowych szczegółów dla wbudowanego generatora. Ten mechanizm działa podobnie do generowania kodu przez IDE, jednak jego główną przewagą jest automatyczne przegenerowanie kodu i trzymanie go zawsze aktualnym w stosunku do nowo dodanych pól klasy.

Dodatkowo AV jest bardzo ciekawym przykładem wykorzystania Java custom annotation processing, który jest standardowym mechanizmem Javy. Powoduje to, że biblioteka jest ciekawą alternatywą dla Lombok, który opiera swoje działanie na potencjalnie ryzykownym wykorzystaniu wewnętrznej implementacji JDK.

Ogólne wrażenia z wykorzystania biblioteki oceniam bardzo pozytywnie. Korzysta się z niej bardzo przyjemnie i stosunkowo prosto.

 

kierunek java


20+ BONUSOWYCH materiałów z programowania

e-book – „8 rzeczy, które musisz wiedzieć, żeby dostać pracę jako programista”,
e-book – „Java Cheat Sheet”,
checklista – „Pytania rekrutacyjne”
i wiele, wiele wiecej!

Jak zostać programistą

No comments
Share:

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *