Рубрики
Без рубрики

Эффективная Java! Предпочитайте аннотации шаблонам именования

Погружение в главу 39 Эффективной Java. Помечено как java, эффективный, аннотация, архитектура.

Исторически создатели библиотек использовали определенные шаблоны именования, чтобы указать, где функциональность должна быть расширена, и в качестве сигнала о том, как мы хотели бы, чтобы библиотека взаимодействовала с нашим кодом. Отличный пример этого – до JUnit 4, чтобы указать, что конкретный метод был тестовым методом; к нему будет добавлено слово test . Этот метод не был лишен своих проблем. Например, орфографическая ошибка может привести к тихим сбоям. Если бы вы назвали свой метод t set Super Important Stuff JUnit не выдал бы никаких ошибок и, к счастью, просто проигнорировал бы ваш тест. Если бы вы не были внимательны, вы бы тоже не заметили. В этой системе нет способа указать, где допустим конкретный шаблон именования. Например, если пользователь подумал, что он вызвал свой класс Протестируйте Все Вещи это подняло бы весь класс, у библиотеки нет способа указать пользователю, что этот шаблон используется не так. Последняя проблема, которую мы собираемся подчеркнуть, заключается в том, что нет простого способа передать параметры потребителю шаблона. Представьте себе попытку передать ожидаемый тип исключения? Что-то вроде test RuntimeException, Вызванного Полезным Тестом

Так какова же альтернатива? Аннотации – вот чем мы можем заменить эти шаблоны. JUnit также решил сделать это с выпуском версии 4. Давайте представим, что мы делаем самый простой из возможных тестовых запусков. Мы хотели бы начать с чего-то, что указывало бы, какие методы следует тестировать.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}

Мы используем ключевое слово @interface , чтобы указать, что мы создаем аннотацию. Затем мы аннотируем нашу аннотацию с помощью мета-аннотаций . Первый из них @Retention(Политика удержания. ВРЕМЯ ВЫПОЛНЕНИЯ) означает, что эта аннотация должна быть доступна для чтения во время выполнения. Если бы мы не добавили эту аннотацию, аннотация исчезла бы до того, как мы смогли бы прочитать ее во время выполнения. Вторая аннотация @Target(ElementType. СПОСОБ) сообщает аннотации, что она может быть помещена только в метод, а не в класс или переменную-член. Давайте посмотрим, как это будет выглядеть на практике.

public class Sample {
  @Test
  public static void m1() { }

  public static void m2() { }

  @Test
  public static void m3() {
    throw new RuntimeException("boom");
  }

  @Test
  public void m4() {
    // invalid usage, not static
  }
}

Теперь давайте рассмотрим пример того, как обработать эту аннотацию.

public class RunTests {
  public static void main(String[] args) throws Exception {
    int tests = 0;
    int passed = 0;
    Class testClass = Class.forName(args[0]);
    for (Method m : testClass.getDeclaredMethods()) {
      if (m.isAnnotationPresent(Test.class)) {
        tests++;
        try {
          m.invoke(null);
          passed++;
        } catch (InvocationTargetException wrappedException) {
          Throwable exception = wrappedException.getCause();
          System.out.println(m + " failed: " + exception);
        } catch (Exception exec) {
          System.out.println("Invalid @Test: " + m);
        }
      }
    }
  }
}

Не очень простой код, но высокоуровневый этот код использует полный путь к классу test. Затем он перебирает методы класса и проверяет, какие из них имеют аннотацию @Test . Затем он вызывает этот метод и проверяет, не генерируется ли исключение. Если нет, он повторяет переданное количество, если да, то выводит причину исключения. Если возникает проблема с вызовом метода тестирования, который также выходит из системы.

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
  Class value();
}

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

@ExceptionTest(ArithmeticException.class)
public static void m1() {
  int i = 1 / 0;
}

Теперь давайте посмотрим, как мы можем это использовать.

if (m.isAnnotationPresent(ExceptionTest.class)) {
  tests++;
  try {
    m.invoke(null);
    System.out.printf("Test %s failed: no exception%n", m);
  } catch (InvocationTargetException wrappedException) {
    Throwable exception = wrappedException.getCause();
    Class exceptionType = m.getAnnotation(ExceptionTest.class).value();
    if(exceptionType.isInstance(exception)) {
      passed++;
    } else {
      System.out.printf("Test %s failed: expected %s, got %s%n", m, exceptionType.getName, exception);
    }
  } catch (Exception exec) {
    System.out.println("Invalid @Test: " + m);
  }
}

Этот код очень похож на наш @Test code runner с небольшими изменениями в логике, чтобы вести себя правильно. Мы видим использование getAnnotation , которое позволяет нам захватить аннотацию и извлечь из нее значение. Допустим, мы хотели перевести этот код на следующий уровень и разрешить выдачу любого из нескольких типов исключений. Наша аннотация потребует лишь небольшого изменения.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
  Class[] value();
}

На самом деле все, что мы сделали, это добавили [] для обозначения того, что тип параметра является массивом. Самое замечательное в этом то, что весь наш существующий код будет продолжать работать с этим изменением. Это связано с тем, что Java создаст массив из одного элемента, если мы просто передадим значение напрямую, как мы это сделали выше. Если мы хотим передать несколько значений в параметр, который принимает массив, мы можем сделать это, поместив их внутрь { } . Давайте посмотрим, как будет выглядеть наш приведенный выше пример:

@ExceptionTest({ArithmeticException.class, NullPointerException.class})
public static void m1() {
  int i = 1 / 0;
}

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

Throwable exception = wrappedException.getCause();
int oldPassed = passed;
Class[] exceptionTypes = m.getAnnotation(ExceptionTest.class).value();
for (Class exceptionType : exceptionTypes) {
  if (exceptionType.isInstance(exception) {
    passed++;
    break;
  }
}
if (passed == oldPassed) {
  System.out.println("Test %s failed: %s %n", m, exception);
}

Это работает довольно хорошо. Однако в Java 8 была введена другая опция, позволяющая использовать несколько значений для конкретной аннотации. Вместо того, чтобы иметь аннотацию, возьмите массив. Мы можем поместить аннотацию на метод несколько раз. Это возможно только с аннотациями, снабженными аннотацией @Repeatable , которая указывает на тип аннотации контейнера, который просто содержит коллекцию этих аннотаций. Давайте посмотрим на это на практике.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
  Class value();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer{
  ExceptionTest[] value();
}

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

@ExceptionTest(ArithmeticException.class)
@ExceptionTest(NullPointerException.class)
public static void m1() {
  int i = 1 / 0;
}

Как вы можете видеть, пользователь повторяющегося типа аннотации не видит тип аннотации контейнера, который просто предназначен для использования кодом обработки. Сложность использования повторяющихся аннотаций заключается в том, что код, который запрашивает состояние аннотаций, не всегда отвечает так, как вы ожидаете. Функция isAnnotationPresent ответит true при проверке типа аннотации коллекции, когда содержащаяся повторяющаяся аннотация используется несколько раз, однако, если это не так, она ответит false и вернет только true если вы проверяете на содержащийся тип. Из-за этого нам нужно проверить как тип коллекции, так и единственную содержащуюся аннотацию. Соответствующая часть нашего кода обработки будет выглядеть примерно так:

if (m.isAnnotationPresent(ExceptionTest.class) || m.isAnnotationPresent(ExceptionTestContainer.class)) {
  tests++;
  try {
    m.invoke(null);
    System.out.println("Test %s failed: no exception%n", m);
  } catch (Throwable wrappedException) {
    Throwable exception = wrappedException.getCause();
    int oldPassed = passed;
    ExceptionTest[] exceptionTests = m.getAnnotationByType(ExceptionTest.class);
    for (ExceptionTest exceptionTest : exceptionTests) {
      if (exceptionTest.value().isInstance(exception)) {
        passed++;
        break;
      }
    }
    if (passed == oldPassed) {
      System.out.println("Test %s failed: %s %n", m, exception);
    }
  }
}

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

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

Оригинал: “https://dev.to/kylec32/effective-java-prefer-annotations-to-naming-patterns-52lk”