Статьи

Java аннотации — большая ошибка

Аннотации были введены в Java 5, и мы все были взволнованы. Такой отличный инструмент, чтобы сделать код короче! Больше никаких конфигурационных файлов Hibernate / Spring XML! Просто аннотации, прямо в коде, где они нам нужны. Нет больше маркерных интерфейсов , только аннотация с сохранением отражения во время выполнения ! Я тоже был взволнован. Более того, я сделал несколько библиотек с открытым исходным кодом, которые интенсивно используют аннотации. Взять, к примеру, jcabi-аспекты . Однако я больше не взволнован. Более того, я считаю, что аннотации — большая ошибка в дизайне Java.

Короче говоря, есть одна большая проблема с аннотациями — они побуждают нас реализовывать функциональность объекта вне объекта, что противоречит самому принципу инкапсуляции . Объект больше не является твердым, поскольку его поведение не определяется полностью его собственными методами — некоторые его функции остаются в другом месте. Почему это плохо? Давайте посмотрим на несколько примеров.

@Inject

Скажем, мы аннотируем свойство с помощью @Inject :

1
2
3
4
5
6
import javax.inject.Inject;
public class Books {
  @Inject
  private final DB db;
  // some methods here, which use this.db
}

Тогда у нас есть инжектор, который знает, что вводить:

01
02
03
04
05
06
07
08
09
10
Injector injector = Guice.createInjector(
  new AbstractModule() {
    @Override
    public void configure() {
      this.bind(DB.class).toInstance(
        new Postgres("jdbc:postgresql:5740/main")
      );
    }
  }
);

Теперь мы создаем экземпляр класса Books через контейнер:

1
Books books = injector.getInstance(Books.class);

Класс Books не знает, как и кто будет внедрять в него экземпляр класса DB . Это произойдет за кулисами и вне его контроля. Инъекция сделает это. Это может выглядеть удобно, но такое отношение наносит большой ущерб всей кодовой базе. Управление потеряно (не инвертировано, но потеряно!). Объект больше не отвечает. Он не может нести ответственность за то, что с ним происходит.

Вместо этого вот как это должно быть сделано:

1
2
3
4
5
6
7
class Books {
  private final DB db;
  Books(final DB base) {
    this.db = base;
  }
  // some methods here, which use this.db
}

В этой статье объясняется, почему контейнеры Dependency Injection в первую очередь ошибочны: контейнеры Dependency Injection являются загрязнителями кода . Аннотации в основном провоцируют нас на создание контейнеров и их использование. Мы перемещаем функциональность за пределы наших объектов и помещаем ее в контейнеры или куда-то еще. Это потому, что мы не хотим дублировать один и тот же код снова и снова, верно? Это верно, дублирование это плохо, но разрывать объект на части еще хуже. Намного хуже. То же самое относится и к ORM (JPA / Hibernate), где аннотации активно используются. Проверьте этот пост, он объясняет, что не так с ORM: ORM — это оскорбительный анти-паттерн . Сами по себе аннотации не являются ключевым мотиватором, но они помогают нам и ободряют нас, разрывая объекты на части и удерживая части в разных местах. Это контейнеры, сессии, менеджеры, контроллеры и т. Д.

@XmlElement

Вот как работает JAXB, когда вы хотите конвертировать ваш POJO в XML. Сначала вы присоединяете аннотацию @XmlElement к получателю:

01
02
03
04
05
06
07
08
09
10
11
12
13
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class Book {
  private final String title;
  public Book(final String title) {
    this.title = title;
  }
  @XmlElement
  public String getTitle() {
    return this.title;
  }
}

Затем вы создаете маршаллер и просите его преобразовать экземпляр класса Book в XML:

1
2
3
4
final Book book = new Book("0132350882", "Clean Code");
final JAXBContext ctx = JAXBContext.newInstance(Book.class);
final Marshaller marshaller = ctx.createMarshaller();
marshaller.marshal(book, System.out);

Кто создает XML? Не book . Кто-то еще, вне класса Book . Это очень неправильно. Вместо этого, вот как это должно было быть сделано. Во-первых, класс, который не имеет представления о XML:

01
02
03
04
05
06
07
08
09
10
class DefaultBook implements Book {
  private final String title;
  DefaultBook(final String title) {
    this.title = title;
  }
  @Override
  public String getTitle() {
    return this.title;
  }
}

Затем декоратор, который печатает его в XML:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
class XmlBook implements Book{
  private final Book origin;
  XmlBook(final Book book) {
    this.origin = book;
  }
  @Override
  public String getTitle() {
    return this.origin.getTitle();
  }
  public String toXML() {
    return String.format(
      "<book><title>%s</title></book>",
      this.getTitle()
    );
  }
}

Теперь, чтобы напечатать книгу в формате XML, мы делаем следующее:

1
2
3
String xml = new XmlBook(
  new DefaultBook("Elegant Objects")
).toXML();

Функция печати XML находится внутри XmlBook . Если вам не нравится идея декоратора, вы можете переместить метод toXML() в класс DefaultBook . Это не важно. Важно то, что функциональность всегда остается на своем месте — внутри объекта. Только объект знает, как напечатать себя в XML. Никто другой!

@RetryOnFailure

Вот пример (из моей собственной библиотеки ):

1
2
3
4
5
6
7
import com.jcabi.aspects.RetryOnFailure;
class Foo {
  @RetryOnFailure
  public String load(URL url) {
    return url.openConnection().getContent();
  }
}

После компиляции мы запускаем так называемый AOP weaver, который технически превращает наш код во что-то вроде этого:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
class Foo {
  public String load(URL url) {
    while (true) {
      try {
        return _Foo.load(url);
      } catch (Exception ex) {
        // ignore it
      }
    }
  }
  class _Foo {
    public String load(URL url) {
      return url.openConnection().getContent();
    }
  }
}

Я упростил реальный алгоритм повторения вызова метода при ошибке, но я уверен, что вы поняли идею. AspectJ , движок AOP , использует аннотацию @RetryOnFailure в качестве сигнала, информирующего нас о том, что класс должен быть включен в другой. Это происходит за кулисами. Мы не видим этот дополнительный класс, который реализует алгоритм повторных попыток. Но байт-код, созданный ткачом AspectJ, содержит модифицированную версию класса Foo .

Это именно то, что не так с этим подходом — мы не видим и не контролируем создание этого дополнительного объекта. Композиция объектов, которая является наиболее важным процессом в проектировании объектов, скрыта где-то за кулисами. Вы можете сказать, что нам не нужно это видеть, потому что это дополняет. Я не согласен. Мы должны увидеть, как составлены наши объекты. Мы можем не заботиться о том, как они работают, но мы должны видеть весь процесс композиции.

Гораздо лучший дизайн будет выглядеть так (вместо аннотаций):

1
Foo foo = new FooThatRetries(new Foo());

И затем, реализация FooThatRetries :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
class FooThatRetries implements Foo {
  private final Foo origin;
  FooThatRetries(Foo foo) {
    this.origin = foo;
  }
  public String load(URL url) {
    return new Retry().eval(
      new Retry.Algorithm<String>() {
        @Override
        public String eval() {
          return FooThatRetries.this.load(url);
        }
      }
    );
  }
}

А теперь реализация Retry :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
class Retry {
  public <T> T eval(Retry.Algorithm<T> algo) {
    while (true) {
      try {
        return algo.eval();
      } catch (Exception ex) {
        // ignore it
      }
    }
  }
  interface Algorithm<T> {
    T eval();
  }
}

Код длиннее? Да. Это чище? Намного больше. Сожалею, что не понял этого два года назад, когда начал работать с jcabi-аспектами .

Суть в том, что аннотации плохие. Не используйте их. Что следует использовать вместо этого? Состав объекта.

Что может быть хуже аннотаций? Конфигурации . Например, конфигурации XML. Механизмы настройки Spring XML — прекрасный пример ужасного дизайна. Я говорил это много раз раньше. Позвольте мне повторить это еще раз — Spring Framework — один из худших программных продуктов в мире Java. Если вы можете держаться подальше от этого, вы сделаете себе большую услугу.

В ООП не должно быть никаких «конфигураций». Мы не можем настроить наши объекты, если они являются реальными объектами. Мы можем только создать их экземпляр. И лучший способ создания экземпляров — оператор new . Этот оператор является ключевым инструментом для разработчика ООП. Отнимать это у нас и давать нам «механизмы конфигурации» — непростительное преступление .

  • Аннотации на Java — большая ошибка (вебинар № 14); 4 мая 2016 года; 744 просмотра; 13 лайков
  • Контейнер внедрения зависимостей — плохая идея (вебинар № 9); 1 декабря 2015 года; 1264 просмотра; 19 лайков
  • Почему геттеры и сеттеры — это анти-паттерн? (вебинар № 4); 1 июля 2015 года; 3095 просмотров; 53 лайков
Ссылка: Java-аннотации — большая ошибка от нашего партнера по JCG Егора Бугаенко в блоге About Programming .