Вычисление имен свойств во время выполнения в языке Java

Некоторые инструменты могут использовать имена свойств виде значений типа String. Обычно они существуют как константы, заданные литералами. Что же не так? А вот что: во время рефакторинга имена свойств могут измениться, более того, свойства могут вообще исчезнуть. А в константах останутся старые, неактуальные значения.

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

Ниже — пример использования инструмента, который родился, когда надоело бегать по граблям вокруг Hibernate'а и его замечательного Criteria API.

package ru.bdm.reflection;

import junit.framework.Assert;
import org.junit.Test;

import java.util.Date;
import java.util.List;

import static ru.bdm.reflection.PathExtractor.Example;

/**
 * User: D.Brusentsov
 * Date: 22.04.13
 * Time: 20:21
 */
public class PathExtractorUsageForHabrahabr {

    public static class Pet {
        private String name;
        private Human owner;

        //getters and setters omitted

    }

    public static class Human {

        private String name;
        private Date birth;
        private List<Human> relatives;

        //getters and setters omitted

    }

    @Test
    public void getPetName() {
        String name = PathExtractor.getPath(new Example<Pet>() {
            @Override
            public void example(Pet pet) {
                pet.getName();
            }
        });

        Assert.assertEquals("name", name);
    }

    @Test
    public void getPetOwnerName() {
        String ownerName = PathExtractor.getPath(new Example<Pet>() {
            @Override
            public void example(Pet pet) {
                pet.getOwner().getName();
            }
        });

        Assert.assertEquals("owner.name", ownerName);
    }

    @Test
    public void getPetOwnerRelativesBirth() {
        String ownerRelativesBirth = PathExtractor.getPath(new Example<Pet>() {
            @Override
            public void example(Pet pet) {
                PathExtractor.mask(pet.getOwner().getRelatives()).getBirth();
            }
        });

        Assert.assertEquals("owner.relatives.birth", ownerRelativesBirth);
    }
}


Итак, у нас есть класс класс со свойствами. Для того, чтобы вычислить динамически имя нужного свойства, мы передаем в метод PathExtractor.getPath экземпляр анонимного класса, который расширяет интерфейс Example. Интерфейс Example определяет одноименный метод, в теле которого нужно обратиться к свойству, чье имя нас интересует.
Если имя свойства меняется руками, то код, вычисляющий это имя, перестает компилироваться. Если же используются инструменты для автоматического рефакторинга, то код изменится автоматически. То есть мы узнаем об ошибке на этапе компиляции или она не произойдет вообще.

Внутри метода PathExtractor.getPath создается прокси, который передается в метод Example.example. Этот прокси запоминает все вызовы, и, если это возможно, возвращает подобный прокси как результат каждого вызова. Таким образом становится возможно узнать не только имя одного свойства, но и построить цепочку имен до любого свойства любого уровня вложенности.

Код выглядит довольно громоздко, но переход на Java 8 с лямбдами или хотя бы на IDE, способную отображать анонимные вложенные классы как лямбды, полностью решает эту проблему.

Минусы:
  • Не работает для методов с модификатором final.
  • Класс, свойства которого мы вычисляем, должен обладать пустым конструктором.


Скачать исходный код можно с Google Drive.

Update:

Вдохновленный комментарием пользователя vladimir_dolzhenko и возможностями Java 8, немного доработал инструмент. Теперь пример использования выглядит более лаконично:

package ru.bdm.reflection;

import org.junit.Test;

import java.util.Date;
import java.util.List;

import static junit.framework.Assert.assertEquals;
import static ru.bdm.reflection.PathExtractorJava8.of;

/**
 * User: D.Brusentsov
 * Date: 22.04.13
 * Time: 20:21
 */
public class PathExtractorJava8UsageForHabrahabr {

    public static class Pet {
        private String name;
        private Human owner;

        //getters and setters omitted
    }

    public static class Human {

        private String name;
        private Date birth;
        private List<Human> relatives;

        //getters and setters omitted
    }

    @Test
    public void getPetName() {
        String name = of(Pet.class, Pet::getName).end();

        assertEquals("name", name);
    }

    @Test
    public void getPetOwnerName() {
        String ownerName = of(Pet.class, Pet::getOwner).then(Human::getName).end();

        assertEquals("owner.name", ownerName);
    }

    @Test
    public void getPetOwnerRelativesBirth() {
        String ownerRelativesBirth = of(Pet.class, Pet::getOwner)
                .thenMask(Human::getRelatives)
                .then(Human::getBirth).end();

        assertEquals("owner.relatives.birth", ownerRelativesBirth);
    }
}


Скачать исходники с Google Drive.
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 11

    +7
    Почему Java-программисты типичные жители default city, постоянно забывают писать о каком языке речь
      +2
      Замечание уместно. Java — не единственный язык на платформе Java. Все остальные намеки позволю себе не заметить. Хорошо?
      0
      Как-то мне тоже потребовалось нечто подобное, только утомляло писать такие длинные простыни — уж больно хотелось краткости и емкости. Воодушевившись mockito и с божьcglib наляпал более емкий синтаксис:

      import static bla.bla.PathExtractor.fake;
      import static bla.bla.PathExtractor.name;
      
      String name = name( fake( Pet.class ).getName() );
      
        0
        Действительно, выглядит более изящно. Из возможных особенностей, поправьте, если я не прав, такой подход применим только с getter'ами.
          0
          Это смотря, что нужно и как сильно можно заморочится. Мне достаточно было самого простого случая — именно getter-ы.
          Но во всем прочем применимы те же идеи, что и в mockito.
        +1
        Более изящно выглядит вариант, когда свойства прописываются в виде строковых констант в классе, а специальный инструмент (например юнит-тест или мавен плагин) просто проверяет, что все такие константы соответствуют полям. В вашем варианте смущает сложность реализации и накладные расходы.
          0
          Это мне кажется самый правильный подход, все равно запросы надо тестировать, а юнит тест убьет двух зайцев сразу. Способ из данной статьи лишь гарантирует соответствие констант и методов, но не гарантирует, что запрос будет работать.
          +1
          С тех пор как IntelliJ IDEA начала поддерживать SQL, HQL, EJBQL и прочие QL, нет никакой потребности в использовании критериев. Более того, критерии делают код громоздким и абсолютно нечитаемым. Так же, нет никакой проблемы с переименованием/удалением свойств.

          Нет-нет, я сам когда-то любил критерии. Динамическое формирование запросов и всё такое. Однажды я даже написал критерий строк на 100 где-то. В нем, правда, кроме меня никто не мог разобраться. Но сейчас — это атавизм, который должен исчезнуть. Ведь HQL такой удобный и няшный, а IntelliJ IDEA его красиво раскрасит и свойства проверит. Ну зачем вам критерии?

          Используйте IntelliJ IDEA.
          Не используйте критерии.
            0
            весьма холиварный по своей сути комментарий
              0
              HQL удобен для небольших статических запросов.

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

              Я не думаю, что ваш запрос в HQL занимал бы намного меньше места или был бы проще для понимания.
                0
                Ну для динамических запросов я и сам критерии использую. Но по мне так, необходимость создать динамический запрос говорит о какой-то проблеме. Конечно, иногда они необходимы, но 1-2 на проект. А если все строится на динамических запросах, значит что-то не так. Вообще, динамические запросы — рассадник ошибок тот еще.

                Насчет понятности, как по мне, так HQL раз в десять понятней и раз в пять короче, чем критерии.

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

            Only users with full accounts can post comments. Log in, please.