Известно, что для создания масштабных высокопроизводительных систем сегодня применяются, в основном, облачные вычисления. Все облака базируются на виртуализации, которая в настоящее время тесно связана с ОС и гипервизорами типа VMware и Xen. Тем не менее, всем известно, что одна из старых и, тем не менее, популярных технологий виртуализации — Java с ее виртуальной машиной, выполняющей байт-коды программы. Поэтому сегодня мы решили рассказать об одной из фишек Java.

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

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

@ Injeсt


Скажем, мы аннотируем проперти с помощью @ Injeсt:
image
Теперь у нас есть инжектор, который знает, что инжектировать:
image
Теперь мы создаем экземпляр класса Books при помощи контейнера:
image
Класс Books не имеет представления о том, кто и как будет инжектировать экземпляр класса DB. Это произойдет «за кулисами» и вне его контроля. Это сделает инжектирование. Возможно, это и кажется удобным, но такое поведение вызывает значительные повреждения во всей основе кода. Происходит не инверсия управления — управление полностью утрачивается. Объект больше не отвечает за то, что с ним происходит.

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

Аннотации сами по себе не являются мотиваторами, но они поощряют разрывание нами объектов и хранение их частей в разных местах, таких как контейнеры, сессии, контроллеры и так далее.

@XmlElement


Вот как работает JAXB, когда вы хотите конвертировать ваш POJO в XML. Для начала вы присоединяете к геттеру аннотацию @XmlElement:
image
Затем вы создаете маршалер и просите его сконвертировать экземпляр classBook в XML:
image
Кто создает XML? Точно не book. Кто-то другой, вне classBook — и это неверно. Вот как это должно было быть сделано. Начнем с того, что класс не имеет представления об XML:
image
Затем декоратор выводит его в XML:
image
Теперь для печати книги в XML мы делаем следующее:
image
Печатный функционал XML — внутри XmlBook. Если вам не по вкусу идея декоратора, вы можете перенести метод toXML() в класс DefaultBook, это неважно. Важно то, что функционал всегда остается там, где он должен быть — внутри объекта. Только объект знает, как напечатать себя в XML, и никто более.

@RetryOnFailure


Вот еще один пример:
image
После компиляции мы запускаем так называемый AOP weaver, который технически преобразует наш код во что-то вроде этого:
image
Алгоритм здесь упрощен, но в целом идея ясна. AspectJ, движок АОП использует аннотацию @RetryOnFailure в качестве сигнала, который информирует нас о том, что класс должен быть обернут в другой. Это также происходит «за кулисами»: мы не видим дополнительный класс, который реализует алгоритм повторной передачи. Но байт-код, произведенный уивером AspectJ содержит модифицированную версию класса Foo.

Это именно то, что неверно в этом подходе — мы не видим и не можем контролировать создание экземпляра дополнительного объекта. Состав объекта, наиболее важный процесс в дизайне объекта, скрыт от нас. Вы можете сказать, что мы и не должны его видеть, так как он дополнительный. Но ведь мы должны знать, как образованы наши объекты? Нас может не волновать, как они работают, но мы должны видеть весь процесс композиции.

Такой дизайн выглядит гораздо лучше (вместо аннотаций):
image
Затем внедрение FooThatRetries:
image
А затем внедрение Retry:
image
Конечно, такой код длиннее, но он куда чище. Аннотации — плохой метод; вместо них используйте лучше композицию объекта.

Что может быть еще хуже, чем аннотации, так это конфигурации. Например, конфигурации XML. Механизмы конфигурации Spring XML — это отличный пример ужасного дизайна.
Не должно быть никаких «конфигураций» в ООП. Конфигурировать объекты, если они реальны, невозможно. Мы можем только инстанцировать их, и лучший метод инстанцирования — оператор new. Это ключевой оператор для разработчика ООП. Его игнорирование и использование «механизмов конфигурации» — просто преступление.
Поделиться с друзьями
-->

Комментарии (43)


  1. Razaz
    25.05.2016 10:46
    +1

    ИМХО аннотации должны быть как в .Net — метаданные. И уже всякие инжекторы и вызыватели пусть смотрят на метаданные класса/метода.


    1. AndreyRubankov
      25.05.2016 11:00
      +4

      Оно так и работает:
      Пишется небольшой «фреймворк», который обеспечивает функционал (инжект зависимостей, конвертировение в / из xml, json т.д.).
      Далее используется этот фреймворк и объекты помечаются аннотациями (метаданными), которые дают возможность фреймворку сделать свою работу.


      1. Razaz
        25.05.2016 12:41

        Я видимо не так понял автора. Извиняюсь.


  1. AndreyRubankov
    25.05.2016 10:54
    +3

    Прошу прощения, но Вы, как и многие другие, не верно используете Inject!

    Этой аннотацией нужно помечать не поля класса, ею нужно помечать Конструктор или же Сеттеры:

    class Books {
    private final Db db;

    @Inject
    public Books(final Db db) {
    this.db = db;
    }
    }

    В данном примере, Вы можете использовать этот класс как с DI контейнером, так и без него!
    Но большинство java-разработчиков по той или иной причине не хотят идти этим путем, в частности любители Spring и его @Autowired

    Все DI контейнеры, согласно спецификации, должны поддерживать этот вариант.
    Guice без проблем выполнит инжектирование в конструктор.


    1. Losted
      25.05.2016 11:37

      А в чем «неправильность» установки на филды? Когда все инжекты в «шапке» класса — их читать удобнее, чем скролля все сеттеры. А спринг под капотом все равно сначала попробует найти сеттер, насколько я помню, а уже потом будет пытаться всунуть значение непосредственно в филд.


      1. AndreyRubankov
        25.05.2016 11:58
        +1

        Проблема, которая порождается из-за такого подхода, описана в первом разделе статьи:
        — сервис, созданный с таким подходом (без сеттеров и конструкторов) невозможно использовать вне контекста DI контейнера. Яркий пример — юнит тесты, которые не работают без инжектов через mockito или через спринг контексты.

        Это удобно, красиво, но не правильно.


        1. Losted
          25.05.2016 12:01

          Вы меня не правильно поняли. Сеттеры должны быть все равно. Я говорю исключительно про расположение аннотации


          1. AndreyRubankov
            25.05.2016 12:24

            Провел небольшой тест: Autowired на поле класса будет делать инжект в обход сеттеров (если у вас нету xml конфигурации, которая через сеттеры работает).


            1. Losted
              25.05.2016 13:46

              Тут-то я и напутал, спасибо.


            1. alist
              25.05.2016 20:58

              Tсли мы просто инжектим сервис внутрь другого сервиса — а обычно это так и происходит, — то геттер-сеттер всегда будут пустыми, без логики: просто "прочти из поля" и "запиши в поле". Тогда нет разницы, игнорируется ли сеттер или нет.


              Я не против сеттеров с логикой и не против DI, но имхо @inject на сеттере с логикой — это перебор с "магией" и неочевидностью.


      1. Razaz
        25.05.2016 12:46
        +4

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

        Инъекции в свойства(филды) могут применяться только в случае каких-то специфичных cross-cutting функциональностей с обязательным заданием дефолтов.

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


        1. Losted
          25.05.2016 13:41

          Имеет смысл, хотя смена реализаций на рантайме — вполне валидный сценарий, когда хочется управления через JMX.


          1. Razaz
            25.05.2016 13:47

            Тогда вы грохаете объект, и создаёте новый. Или используете провайдер, у которого можно переключать поведение в качестве штатного апи. Тогда и потребитель и пользователь понимают что происходит.
            Я просто не знаю фич Java контейнеров, но в .Net + Autofac активно пользуюсь скоупами(на хост, на операцию, на какую-то бизнес транзакцию).


            1. Losted
              25.05.2016 14:05

              Да понятно, что реализаций может быть больше одной — главное чтобы они были предсказуемы в поведении. Будет ли это контракт на уровне бина/контекста/интерфейса класса уже не так важно. Собственно у меня был концептуальный вопрос по поводу, «почему лучше». Ответ меня уже вполне удовлетворил и я с ним согласился с некоторыми поправками на то, что иногда все-таки надо иначе.

              На своих проектах я, в основном, использовал инъекцию напрямую в филд без сеттеров. Идея с конструктором интересна, но в случае, когда управление объектами идет только через контейнер, возможно, просто избыточна — вручную конструкторы все равно никто вызывать не будет, так что все зависимости инкапсулированы в классе бина и инициализированы в рамках контекста приложения. Хотя утверждение про четкость и честность контракта на уровне сигнатур класса мне нравится — надо его обдумать.

              Я, к сожалению, про .Net вообще ничего не знаю и не могу с вами на таком уровне подискутировать.


              1. Razaz
                25.05.2016 14:14

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

                Бывают кейсы, когда ну никак не выкрутиться и приходится делать ручную сборку объекта. Или ресолвить что то в конструктор во время выполнения.

                Ну .Net почти то же самое только в профиль :)))


                1. Losted
                  25.05.2016 15:24

                  Вот тут есть большая разница между разработкой реюзабельной библиотеки и приложения. В приложении вполне можно сделать предположение, что IoC-контейнер — это часть приложения. Более того, при использовании аннотаций оно все равно так или иначе протекает. В таком случае способы вызова класса уже вполне предсказуемы. И тогда, если отталкиваться от принципов YAGNI и KISS, то конструктор уже лишний. Более того, в достаточно большом количестве приложений инициаций инстансов «своих» классов через конструктор из кода вообще не происходит — только через DI. В таком случае, как мне кажется — это не вопрос практической разработки и просто философская дискуссия о чистоте подхода.


                  1. Razaz
                    25.05.2016 17:05

                    Можно не использовать аннотации :)

                    разве нельзя заставить контейнер выбирать конструктор по умолчанию?
                    Например:

                    public class Foo
                    {
                        public Foo(Service1 service1, Service2 service2)
                        {...}
                    }
                    

                    Зачем тут аннотации?


                    1. Losted
                      25.05.2016 17:14

                      Можно настроить через XML-конфигурацию, например, в духе старой школы. Но не ухудшит ли это читаемость кода? Я отлично помню огромное количество конфигурационных файлов или даже один, но здоровенный на старых проектах. С радостью заменю их на «протекающую аннотацию».

                      В конце-концов тут же практический вопрос: стоит ли жертвовать скоростью разработки или читаемостью ради абстрактной идеологической чистоты кода?


                      1. lair
                        25.05.2016 18:07

                        Можно настроить через XML-конфигурацию

                        А можно просто выбрать convention over configuration.


                      1. Razaz
                        25.05.2016 18:45

                        Autofac — Вот пример без xml и без аннотаций. Правда под .Net. Но почему нельзя на Java так же не понимаю.


  1. lair
    25.05.2016 10:55
    +12

    Кто создает XML? Точно не book. Кто-то другой, вне classBook — и это неверно

    Подождите-подождите, но как же Separation of Concerns?


    Мы можем только инстанцировать их, и лучший метод инстанцирования — оператор new.

    Подождите-подождите, но как же Dependency Inversion?


    Аннотации — это всего лишь метаданные. Автору текста не нравится вынесение cross-cutting-concerns в отдельный код? Значит, ему предстоит много дублирования кода; а дублирование, в свою очередь, будет приводить к ошибкам.


  1. Vurtatoo
    25.05.2016 11:00
    +3

    Многие из ваших недостатков некоторые именуют как достоинства.


  1. lpre
    25.05.2016 11:11
    +2

    @Inject
    private final DB db;

    Класс Books не имеет представления о том, кто и как будет инжектировать экземпляр класса DB. Это произойдет «за кулисами» и вне его контроля. Это сделает инжектирование.… Объект больше не отвечает за то, что с ним происходит.

    А вот как это должно было быть сделано:
    private final DB db;
    Books(final DB base) {
    	this.db = base;
    }
    
    А в чем, собственно, разница с точки зрения «ответственности объекта за то, что с ним происходит»? В обоих случаях объект DB инстанциируется снаружи нашего объекта, а затем «инжекируется» в него — просто двумя разными способами: через аннотацию и конструктор. Ни в одном из вариантов объект Books не контроллирует создание объекта DB и «не имеет представления о том, кто и как будет инжектировать экземпляр класса DB». Чем второй вариант лучше первого?

    Автор, поясните свою мысль, pls.


  1. gurinderu
    25.05.2016 11:18

    А как вообще работает Inject на final field? Насколько я помню, поле должно быть не final иначе просто не скомпилируется.


    1. lpre
      25.05.2016 11:26

      Верное замечание. Инжектировать в final field можно только через конструктор:

      private final Db db;
      
      @Inject
      public Books(Db db) {
      	this.db = db;
      }


    1. AndreyRubankov
      25.05.2016 12:03

      Guice, используемый в примере, делает это через рефлексию, final и любая другая сигнатура не является помехой в этом случае (Guice даже в статические поля инжектить может).
      ps: Аннотация Inject обрабатывается в рантайме и только в DI контейнере.


      1. lpre
        25.05.2016 12:17

        До рефлексии дело вообще не дойдет: класс, в котором объявлено, но не инициализировано помеченное как final поле, просто не скомпилируется.


        1. AndreyRubankov
          25.05.2016 14:49

          и то верно, не досмотрел.
          но в примере — картинка, которая умалчивает, какой там конструктор, может там таки есть конструктор, который делает нормальную инициализацию =)


  1. endemic
    25.05.2016 11:39

    Конфигурации сложились в то время, когда деплой приложения был сложен, и разработчикам часто приходилось делать подстройку параметров на рабочей машине. Сейчас с использованием непрерывной доставки конфигурация в коде и правда более удобна, и спринг постепенно переходит на нее. Что же касается аннотаций, то это скорее вопрос вкусовщины. НАпример мне аннотации нравятся, и я бы хотел иметь возможность делать Generic аннотации, в которые можно передать функции. Другое дело что аннотации дают Java декларативность и приближают ее к функциональным языкам, и возможно просто надо использовать более гибкий язык.


  1. NonGrate
    25.05.2016 11:55

    Есть ещё препроцессоры. Например, Lombok, который видя аннотации, добавляет код в сам класс.

    Или dependency injection. Если посмотреть, разницы между передачей инстанса в конструктор и передачей инстанса через Inject, особо нет. В обоих случаях кто-то сторонний обрабатывает и подготавливает инстанс.

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


  1. Throwable
    25.05.2016 12:22

    Кто создает XML? Точно не book. Кто-то другой, вне classBook — и это неверно.

    С чего бы неверно? Фукционал класса Book — определить структуру и уметь содержать данные. Book вообще не должен ничего знать ничего том, кто и как его будет сериализовать. Поэтому правильный подход — это держать отдельно от объекта дескриптор метаданных, который при необходимости будет использоваться фреймворком для сериализации.


    Но тут уж палка о двух концах: либо ваши объекты и метаданные описываются в одном месте, их удобно рефакторить и читать, но это нарушает принцип разделения ответственности; либо вы держите отдельно класс и дескриптор метаданных и мучаетесь с описанием и синхронизацией имен. И проблема в том, что в Java нет хорошего метода сослаться извне на поля и методы класса, поэтому в качестве внешнего описания выбран XML.


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


    • Клиент должен знать о JPA: все JPA-аннотации должны быть у него в classpath (зачем, если это клиент?)
    • Разные фреймворки, работающие с одними сущностями, очень плохо интегрируются друг с другом. Долгое время была проблема подружить JAXB и Hibernate. Извращались, ставя Hibernate-аннотации на геттеры, а JAXB на поля.
    • На стороне сервера модифицированные классы совсем не те, что сгенерил компилятор и что лежит у клиента. Большинство сериализаторов валится из-за наличия дополнительных полей и фреймворк-зависимых типов.

    Есть и альтернативный подход: разработать некий метаязык для описания модели и метаданных, а потом из него генерить отдельно Java-классы для каждого фреймворка по необходимости. Это то, как работает Eclipse Modelling Framework, Texo, Epsilon, etc...


    1. sshikov
      25.05.2016 19:51

      Генерить это тоже не выход. На самом деле автор (и многие комментирующие) похоже забывает о том, что бывают разные retention policy, и аннотация может быть как механизмом только времени компиляции, так и для runtime. И это два совершенно разных механизма, хотя и выглядят почти одинаково.


  1. tangro
    25.05.2016 12:45

    Объект больше не отвечает за то, что с ним происходит.

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


    1. AndreyRubankov
      25.05.2016 21:23

      Бывает и по-другому. Это называют DDD (Domain Driven Development), в котором объекты сами решают, что они должны уметь делать. Прям как ООП по учебнику, где данные и методы работы с данными в одном объекте находятся.

      Но сейчас тенденции к тому, чтобы отделить данные от логики работы над ними: есть DTO и есть service, первые содержат данные, но не содержат логики, вторые — содержат логику, но чаще всего не содержат данных (stateless).


      1. lair
        25.05.2016 22:50
        +1

        Это называют DDD (Domain Driven Development), в котором объекты сами решают, что они должны уметь делать.

        И что, в DDD логика сохранения объекта в БД лежит в самом объекте?


        1. AndreyRubankov
          26.05.2016 07:23

          А ответственность сохранения себя в БД уже называют Active Record (хотя, не мне Вам рассказывать).

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


  1. iamironz
    25.05.2016 12:58
    +6

    Статья Егора и без тега #trueoop? Расходимся, наброс не засчитан.


  1. Terranz
    25.05.2016 15:28

    так так ладно
    а что взамен?


  1. molchanoviv
    25.05.2016 16:10
    +1

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

    WAT? Это же прямое нарушение принципа Single Responsibility. Объект не должен сам себя сериализовывать в XML. Вы когда открываете дверь не говорите ей открыться, а встаете и открываете ее. Тут принцип аналогичен


  1. CoyoteCoder
    25.05.2016 16:10
    +2

    > Класс Books не имеет представления о том, кто и как будет инжектировать экземпляр класса DB.

    А почему вы решили что он должен знать? Separation of concerns. Это не его ответственность, и его поведение зависит только от интерфейса DB.
    Тот факт, что в проперти лучше не делать инъекцию зависимостей, слабо связан с аннотациями. Еще, кстати большой вопрос, зачем вам зависимость от БД в классе «книга»? И, IMHO, книга не должна зависит от способа ее хранения, а значит зависимости от БД быть в этом классе не должно.

    > Кто создает XML? Точно не book. Кто-то другой, вне classBook — и это неверно.

    Это как раз верно. И опять же не связано с аннотациями. Создание представления — это совершенно отдельное поведение, никак не связанное с самим классом. Тот же самый Separation of concerns.

    > Только объект знает, как напечатать себя в XML, и никто более.

    Объект не должен этого знать. Так же как создание библиотечной карточки не является поведением реальной книги.

    > Это именно то, что неверно в этом подходе — мы не видим и не можем контролировать создание экземпляра дополнительного объекта. Состав объекта, наиболее важный процесс в дизайне объекта, скрыт от нас.

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

    retry(AtMost(3), _ => doSomething(args))

    никаких аннотаций, никаких головоломок, и не нужно каждый раз печатать по 10 строк кода, которые не нужны.


  1. fogone
    25.05.2016 16:32
    +2

    1. Аннотации здесь вообще ни причем — аннотации это инструмент. DI — это механизм, который использует аннотации для своей работы. Говорить, что «аннотации — это одна большая ошибка», имея ввиду, что кому-то не нравится, как их где-то используют — вот это и есть одна большая ошибка.
    2. Запомните или запишите что ли себе где-нибудь: каждый раз, когда вы пишете new в своем коде, умирает рыбка.
    3. Если эта статья не троллинг, то смысл статьи, краткое содержание которой: «говно-говно-говно» не очень понятен, да и вызывает вопросы о компетентности писавшего. Нет, буду думать, что это троллинг.


  1. Alex_kk
    25.05.2016 22:11

    Зачем смешивать концепции аспектного программирования и ООП?
    Аннотации — элемент аспектного стиля программирования, который действительно создает некоторый уровень неочевидности (аспектные техники отрабатывают уже после компиляции объектного кода, то есть действительно исходный код классов не может контролировать то, что возникает на уровне аспектов).
    Соответственно, как обычно, если использовать аннотации правильно (тот же Inject на конструкторе или правильная разметка полей и/или get-методов для согласования с внешним маршаллером в XML), это улучшает код и упрощает работу с ним. Если аннотации расставлять бездумно, получим ухудшение кода (впрочем, как и в случае бездумного использования других техник программирования).
    Да, а сама идея отделения логики приложения от настроек и правил конфигурирования модулей для больших приложений очень разумна. Достаточно вспомнить, что давным-давно контейнеры приложений Java EE умеют внедрять зависимости посредством сервисов наподобие JNDI (т.е. логика приложения изначально отделяется от его настройки и администрирования).
    Так что аргументация в статье выглядит весьма сомнительной.


  1. warlock13
    26.05.2016 02:34
    +1

    class XmlBook implements Book

    Как сказал Сальвор Хардин, «наследование — последнее убежище беспомощного». Перетаскивать же всё в DefaultBook — тоже, мягко говоря, не особо хорошее решение в большинстве случаев.