воскресенье, 21 мая 2017 г.

Java Annotations Are a Big Mistake

Аннотации были введены в Java 5, и мы все были в восторге. Такой замечательный инструмент, чтобы сделать код короче! Больше файлов конфигурации Hibernate / Spring XML! Прямо там, где нам нужны эти аннотации. Нет больше  marker interfaces, просто, runtime-retained reflection-discoverable annotation! 

Я тоже был воодушевлен. Более того, я сделал несколько библиотек с открытым исходным кодом, которые используют аннотации в значительной степени. Возьмем, например, jcabi-аспекты. Однако теперь я больше не воодушевляюсь. Более того, я считаю, что аннотации - большая ошибка в дизайне Java.

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

@Inject

Say we annotate a property with @Inject:

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

Then we have an injector that knows what to inject:

Injector injector = Guice.createInjector(
  new AbstractModule() {
    @Override
    public void configure() {
      this.bind(DB.class).toInstance(
        new Postgres("jdbc:postgresql:5740/main")
      );
    }
  }
);

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

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

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

class Books {
  private final DB db;
  Books(final DB base) {
    this.db = base;
  }
  // some methods here, which use this.db
}

В этой статье объясняется, почему контейнеры инъекции Dependency являются неправильной идеей, в первую очередь: контейнеры инъекции зависимостей являются загрязнителями кода.


Аннотации в основном провоцируют нас делать контейнеры и использовать их. Мы перемещаем функциональность за пределы наших объектов и помещаем их в контейнеры или где-то еще. Это потому, что мы не хотим дублировать один и тот же код снова и снова, верно? Это правильно, дублирование плохо, но разрывание объекта на части еще хуже. 

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

@XmlElement

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

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;
  }
}

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

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. Это очень неправильно. Вместо того, как это должно было быть сделано. Во-первых, класс, который не имеет представления о XML:

class DefaultBook implements Book {
  private final String title;
  DefaultBook(final String title) {
    this.title = title;
  }
  @Override
  public String getTitle() {
    return this.title;
  }
}

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

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, мы сделаем следующее:

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

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

@RetryOnFailure

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

import com.jcabi.aspects.RetryOnFailure;
class Foo {
  @RetryOnFailure
  public String load(URL url) {
    return url.openConnection().getContent();
  }
}

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

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, механизм АОП, использует @RetryOnFailure аннотацию в качестве сигнала, информируя нас, что класс должен быть завернут в другой. Это происходит за кулисами. Мы не видим дополнительный класс, реализующий алгоритм повторения. Но байт-код, созданный AspectJ weaver, содержит модифицированную версию класса Foo.

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

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

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

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

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:

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. Этот оператор является ключевым инструментом для разработчика ООП. Изъятие его у нас и предоставление нам «механизмов конфигурации» - непростительное преступление.

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

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