Далее будет описание ключевых проблем возникающих от использования одной из техник Inversion of Control (IoC): Dependency Injection (DI). Проблемы как проистекают как из самой идеи DI, так и из применения оной на языке java.
Первым делом небольшое соглашение: в данном случае полагается, что в DI имеет смысл помещать исключительно объекты подходящие под определение «сервиса». Для сервиса характерно:
- Отсутствие или сокрытие состояния, напр. сервис выполняющий сериализацию в json, сервис выполняющий шифрование и сервис доступа к бд, кеш.
- Разделение API: публичное (собственно API сервиса, как правило за него отвечает отдельный интерфейс) и управления (сюда входит и управление жизненным циклом сервиса и конфигурация)
- Наличие характерных стадий жизненного цикла: создание реализации -> конфигурация -> работа (или обработка запросов) -> завершение
С учетом того что API управления должно быть скрытым от пользователя сервиса, то наиболее краткий вариант реализации сервиса будет в виде: http://ic.pics.livejournal.com/wayerr/33550674/1829/1829_900.png Допустим нашему сервису требуется доступ к парочке иных сервисов: http://ic.pics.livejournal.com/wayerr/33550674/1590/1590_900.png
Эта очевидная и удобная схема логично проистекает из самого языка программирования и правил хорошего кода (не всех). Однако в реальности разработчик столкнется с ситуацией когда у сервиса появится дополнительная зависимость, если это в рамках одного проект - то нет никаких проблем, в конструктор легко добавить параметр. Если же дело происходит в библиотеке которую используют другие проекты, то конструктор менять нельзя - это сломает API. Можно сделать второй конструктор, но множество DI фреймворков не поддерживают это (хуже того их поведение может быть не определено в этом случае).
Отлично, раз нам нужна расширяемость то используем конфигурацию сервиса через сеттеры: http://ic.pics.livejournal.com/wayerr/33550674/2102/2102_900.png На этом большинство разработчиков удовлетворяется. Но нет, проблемы подкрались незаметно:
- Сервис не имеет стадии инициализации - т.е. у нашего сервиса нет той точки в коде, после которой можно с уверенностью заявить, что он корректно сконфигурирован. При этом конструктор объекта уже не выполняет своей роли.
- API конфигурации публичен, а значит возможно изменение конфигурации в процессе работы. Особенно много радости от отладки эта возможность подарит в многопоточном окружении.
- Так как очень часто возникает необходимость в выполнении некоторых действий после инициализации то появляются затейливейшие костыли вроде методов аннотированных @PostConstruct.
Чтобы избавится от этих проблем надо четко выделить стадию инициализации, нетрудно догадаться что для инициализации служит конструктор, значит вернемся к инициализации через конструктор, но немного другим образом: http://ic.pics.livejournal.com/wayerr/33550674/2359/2359_900.png
На диаграмме видно новый объект: конфигурация сервиса, он содержит только ссылки на объекты необходимые для инициализации сервиса. При таком подходе мы сохраняем расширяемость (в конфигурацию всегда можно добавть еще свойство). Вновь выполняет свои задачи конструктор сервиса, в нем теперь можно валидировать конфигурацию. API конфигурации скрыто от пользователя и сервис уже нельзя переконфигурировать во время работы.
Напоследок стоит отметить две вещи:
- Большинство DI контейнеров, не поддерживают такой подход без дополнительной доработки.
- Получившийся дизайн сервиса можно упростить если использовать Service Locator который передавать в конструктор, при этом мы получим все теже достоинства но без написания лишнего кода. Да, это уже не будет IoC, зато сохраняются все его преимущества и теряются недостатки.
зы. Да это опять я, и по традиции с картинками тут.
ps2. http://thecodelesscode.com/case/104?lang=ru&trans=smal - для тех кто скажет про документацию к сервису