...

вторник, 11 марта 2014 г.

Трансформации AST — Первый шаг к тяжёлым веществам

А давайте сделаем magic с вашим Java кодом. Вот такой:


Берем это:



import groovy.transform.Canonical
import groovy.transform.TupleConstructor

@Canonical
@TupleConstructor
class Person {
int id
String firstName
String lastName
Date birthdate
}




Компилируем, и в байткоде получаем аналог вот этого:

Адский бойлерпейт на Джаве на 100 с лишним строк


import java.util.Date;
import java.util.Map;

public class Person {
private int id;
private String firstName;
private String lastName;
private Date birthdate;

//Эта штука добавлена @TupleConstructor-ом
public Person(Map parameters){
this.id = (int) parameters.get("id");
this.firstName = (String) parameters.get("firstName");
this.lastName = (String) parameters.get("lastName");
this.birthdate = (Date) parameters.get("birthdate");
}

public Person(int id, String firstName, String lastName, Date birthdate) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.birthdate =birthdate;
}

public Person(int id, String firstName, String lastName) {
this(id, firstName, lastName, null);
}

public Person(int id, String firstName) {
this(id, firstName, null, null);
}

public Person(int id) {
this(id, null, null, null);
}

public Person() {
this(0, null, null, null);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;

Person person = (Person) o;

if (id != person.id) return false;
if (birthdate != null ? !birthdate.equals(person.birthdate) : person.birthdate != null) return false;
if (firstName != null ? !firstName.equals(person.firstName) : person.firstName != null) return false;
if (lastName != null ? !lastName.equals(person.lastName) : person.lastName != null) return false;

return true;
}

@Override
public int hashCode() {
int result = id;
result = 31 * result + (firstName != null ? firstName.hashCode() : 0);
result = 31 * result + (lastName != null ? lastName.hashCode() : 0);
result = 31 * result + (birthdate != null ? birthdate.hashCode() : 0);
return result;
}

@Override
public String toString() {
return "Person{" +
"id=" + id +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", birthdate=" + birthdate +
'}';
}

public int getId() {
return this.id;
}

public void setId(int paramInt) {
this.id = paramInt;
}

public String getFirstName() {
return this.firstName;
}

public void setFirstName(String paramString) {
this.firstName = paramString;
}

public String getLastName() {
return this.lastName;
}

public void setLastName(String paramString) {
this.lastName = paramString;
}

public Date getBirthdate() {
return this.birthdate;
}

public void setBirthdate(Date paramDate) {
this.birthdate = paramDate;
}
}





Ну, как-бы да, приятно. Но ничего уникального, вот-же есть Lombok, не говоря уже о способности любого хорошего IDE сначала генерить, а потом прятать всесь этот бойлерплейт.


Так зачем именно Groovy, почему AST transformations?

В этой статье я попробую вкраце обосновать, зачем пользоваться Groovy AST transformations в Java проектах, и (опять-же вкраце) рассказать какие AST transfromations есть в Groovy сегодня. Если вы уже знаете зачем, и хотите только «как и что», смело листайте к «Введение в AST transformations».



Итак, почему AST transformations а не Lombok?




Начнем с того, что для того чтобы пользоваться AST transformations вам не нужно ни знать Groovy, ни писать на Groovy, ни запускать Groovy в рантайме. Трансформация происходит во время компиляции сорцов и Groovy добавляется одним jar-ом в список зависимостей. Всё.

Таким образом, AST transformations являются прекрасным способом «протащить» Groovy в ваш проект: «Смотри, босс, это ничего страшного, это просто ещё одна библиотека для борьбы с бойлерплетом!». А потом, уже, конечно, шаг за шагом, тестом на Споке, за билдом на Грейдле, в вашем коде появится настоящий Groovy — динамический, фунциональный и элегантный. AST transformations это только первый шаг.

Кроме того, AST transformations намного более расширяемы, мощны и универсальны, чем Ломбок.

Последнее, но не менее важное — AST transformations прекрасно поддерживаются в любом IDE с поддержкой Groovy, а не только в Эклипсе.

Фронтальное сравнение Ломбока с AST transformations явно выходит за рамки этой статьи, так что на этом остановимся.

Мне кажется само собой разумеющимся, что генерация байткода имеет огромное преимущество над генерацией (а потом «схлопыванием» в редакторе чтобы не мозолило глаза) исходного кода — генерация происходит во время компилирования, её не нужно «поддерживать». Один пример — IntelliJ IDEA прекрасно генерирует hashcode и equals. При добавлении нового поля, я ручками стираю эти 2 метода и генерю их заново. Фу.


Можно ещё многое сказать о преимуществах AST transformations для разработчиков как Java, так и Groovy, но, я надеюсь, идея ясна. Пора переходить к практике.


Введение в AST transformations




Одна из самых главных плюшек в Groovy, это, конечно метапрограммирование. Оно бывает двух типов — во время компиляции и во время исполнения.

Метапрограммирование во время исполнения это примерно «ах, вы вызвали метод, которого не существует? Не страшно, мы сейчас чего-нить придумаем, на основе того, что вы имели ввиду, когда вызывали этот метод». Примеров такого масса — практически любая библиотека Groovy основана на таких штуках, будь то билдеры, сларперы, Grails, Ratpack, Gradle, и все остальное. Но, сейчас не об этом (если хотите об этом, смотрите пункт 1 наглого пиара в конце поста).

Сейчас мы поговорим о метапрограммировании во время компиляции, а именно о том, как просто написав в коде одно, в байткоде получить другое (ну, или дополнительное).


Начнем мы с трансформации, которая прошита прямо в самом Groovy, без всяких аннотаций и прочих добавок.

Пишем:



class Person {
String name
}




На выходе получаем байткод, в котором все поля — private (в данном случае — name), и прописаны все getters и setters (ну, в данном случае только getName() и setName(String name), но идея ясна).

Эта прекрасная мелочь является полноценным примером метапрограммирования во время компиляции.


Посмотрев на это небольшое избавление от бойлерплейта, чудесный человек Danno Ferrin сказал себе: «Но ведь есть ещё много бойлерплейта, кроме геттеров и сеттеров, и не у всех они одинаковые! Давайте-ка придумаем чего-нить подключаемое и расширяемое!» И так родились AST Transformations (первая, как ни странно, была @Bindable . Хотя, если посмотреть, сколько кода она выкидывает, может и не странно).


AST transformations это набор аннотаций, которые меняют абстрактное синтаксическое дерево налету во время компиляции Groovy. Можно сказать, что добавление getters и setters это встроенная трансформация AST, которая работает всегда, без добавления аннотаций. Остальные-же включаются только по требованию.


Давайте посмотрим что у нас есть:



  • Итак, аннотация-пионер @Bindable и её подруга @Vetoable превращают геттеры и сеттеры в настоящие properties, с возможностью нацеплять на них listener-ы и слушать, регаривать и запрещать изменения.

  • Очень модные нынче словечки @Category и @Mixin добавляют природу одного класса в другой класс. Ну, примеси!

  • @Delegate добавляет все методы, которые существуют у делегата, имплементируя их, натурально, делегацией, да?

    Пишем:

    class Event {
    String name
    @Delegate Date date
    }




    Получаем готовую делегацию


    import java.util.Date;

    public class Event {
    private String name;
    private Date date;

    public String getName() {
    return this.name;
    }

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

    public Date getDate() {
    return this.date;
    }

    public void setDate(Date paramDate) {
    this.date = paramDate;
    }


    public boolean after(Date otherDate) {
    return date.after(otherDate);
    }

    public boolean before(Date otherDate) {
    return date.before(otherDate);
    }

    public long getTime() {
    return date.getTime();
    }

    public void setTime(long timestamp) {
    date.setTime(timestamp);
    }
    }







  • @Immutable делает класс неизменяемым, а конкретно:


    1. сеттеры кидают ReadOnlyPropertyException

    2. класс становится final

    3. поля становятся private и final

    4. появляются конструкторы со всеми полями: как просто в параметрах, так и в мапе (как в первом примере)

    5. появляется код, который создает резервные копии для изменяемых компонентов

    6. появляются equals, hashcode и toString




  • Вот ещё пачка подобных борцов с бойлерплейтом: @InheritConstructors добавляет все конструкторы из супер-класса, @TupleConstructor добавляет конструктор map, в котором ключи — названия полей, а значения — значения (см. первый пример этой стати), @AutoClone и @AutoExternalize добавляет соответствующие методы, а @Canonical делает «правильный Джава класс» — с конструктором без параметров, конструкторами, которые все параметры принимают (как подряд, так и мапой), и equals, hashCode и toString-ом. Ну, как @Immutable , только mutable — его мы тоже видели в первом примере.

  • Ещё один модный термин! @Lazy создаст лениво-инициализруемое поле (по первому требованию), опционально, обвернутое в soft-reference

  • @Newify позволяет создавать объекты с помощью метода new вместо названия конструктора (как в Руби), или, наоборот, только по названию конструктора, без new (как в Пайтоне). Тут, пожалуй, не помешает пример:

    @Newify rubyLikeNew() {
    assert Integer.new(42) == 42
    }




    или даже

    @Newify([Tree, Leaf]) buildTree() {
    Tree(Tree(Leaf(1), Leaf(2)), Leaf(3))
    }




    В последнем примере мы создаем Tree и Leaf без использования new. Сравните с аналогом на Java:

    public Tree buildTree() {
    return new Tree(new Tree(new Leaf(1), new Leaf(2)), new Leaf(3));
    }




  • А вот вам исправление давней несправедливости: в Груви по умолчанию все поля public. A как сделать package? Через @PackageScope трансформацию!

  • Вне зависимости от того, считаете ли вы Singleton паттерном, или анти-паттерном, иногда приходится его писать. Ну, или просто поставить @Singleton над классом, и лениво-инициализируемый синглтон с двойной проверкой локинга готов.

  • Наш, #razborpoletov-ный Андрей написал чудесную, вошедшую в Груви 2.2 @Memoized , которая запоминает результат работы метода, и если он вызывается ещё раз, отдает результат немедленно (и да, параметры имеют значение)

  • И напоследок — аннотация-анекдот @NotYetImplemented — она переворачивает результаты JUnit тестов: те, которые должны падать, проходят, и наоборот. Кроме того, что это напоминает def true=false //happy debugging, эта штука полезна для TDD — накидываем тесты для всех методов, включая те, которые ещё не прописаны, и заставляем тесты, которые ещё не должны и не могут работать, проходить с помощью @NotYetImplemented. Таким образом, падения этих тестов не будут мешать нам тестировать остальные.




И это ещё не всё! Есть ещё архи-важный @CompileStatic , @Field , и целый набор аннотаций для облечения страданий по concurency, но это, всё-же, в другой раз (ну, или, смотрите пункт 1 наглого пиара в конце поста).

P.S. Теперь, когда вы знаете о чем речь, вот вам две интересные хабра-статьи, о том как и чем писать новые AST трансформации. Об этом же смотрите ниже, в пункте 2 наглого пиара.


А теперь наглый пиар из 2 пунктов:



  1. Кому нужно Грувей с нуля и до достаточно продвинутого упора, айда на мои трейнинги, 17-го апреля в Москве и 15-го апреля в Казани (стучать alexbel)

  2. Кому расчленёнки абстрактного синтаксического дерева и написания собственных AST трансформаций для борьбы с вашими собственными тараканами бойлерплейтами, айда на мои доклады на JPoint 18-го апреля и на JavaDay Kazan 16-го апреля (стучать опять alexbel)


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий