Это перевод публикации Ника Ходжеса от 24-09-2011: Getting Giddy with Dependency Injection and Delphi Spring #6 – Don’t even have a constructor.
Все переводы по Spring
- Я не переводил части с 1й по 5ю.
- DI и Delphi Spring. Часть 5. Основы Delphi Spring.
- DI и Delphi Spring. Часть 6. Обойдёмся без конструктора.
- DI и Delphi Spring. Часть 7. Контроль над созданием.
- DI и Delphi Spring. Часть 8. Разное.
- DI и Delphi Spring. Часть 9. Один интерфейс несколько реализаций.
Вступление
В четвёртой статье этой серии я озвучил пару правил, одним из которых было “Делайте Конструкторы Простыми”.(примечание переводчика: я не переводил части 1-4.) В последней статье мы узнали, как использовать контейнер Spring для хранения интерфейсов и реализаций и как запросить у контейнера Spring готовую реализацию интерфейса, вместо создания объекта вручную с помощью конструктора.
В этой статье мы продвинемся ещё на один шаг и посмотрим, как Delphi Spring Framework умеет самостоятельно создавать объекты и автоматически вставлять реализацию в нужных местах, и нам для этого не придется даже вызывать конструктор.
Заметка
В предыдущей статье мы создали модуль uServiceLocator.pas, который предоставлял нам централизованное хранилище для регистрации и поиска реализаций интерфейсов. С тех пор, исходный код Spring-а изменился и теперь предоставляет аналогичный функционал прямо из коробки. Модуль Spring.Services содержит функцию одиночку (singleton) ServiceLocator, которая используется для получения сервисов (даже вызов изменился с Resolve на GetService). Модуль Spring.Container содержит аналогичную функцию GlobalContainer, используемую для регистрации комбинаций интерфейс/реализация. Все объекты, зарегистрированные через GlobalContainer доступны для получения через ServiceLocator.
Маленький обзор
Среди прочего, конструкторы чаще всего используют для создания дополнительных необходимых объектов. В предыдущих статьях мы уже говорили о необходимости уменьшения зависимостей путём отказа от создания объектов в конструкторе, и вместо этого передавая их в качестве параметров. (Примечание переводчика: т.е. параметризация конструктора. Описание метода можно найти в Кратком конспекте книги “Эффективная работа с унаследованным кодом”). Хорошим аргументом в пользу параметризации конструктора является желание ограничить то, что может произойти при создании объектов. Давайте рассмотрим код:
constructor TOrderProcessor.Create; begin FOrderValidator := TOrderValidator.Create; FOrderEntry := TOrderEntry.Create; end;
Этот код выглядит совершенно нормально. Мы создаём экземпляры классов, необходимых для обработки заказа. Но всё-таки здесь есть несколько проблем, которых можно было избежать:
- Мы придерживаемся класса TOrderValidator. Это единственный класс, который может использоваться в TOrderProcessor. Мы лишены возможности выбрать какой-то другой класс или другую реализацию. Аналогично для TOrderEntry.
- Мы не просто придерживаемся, мы с ним связаны. Если TOrderValidator изменится и начнёт вести себя нежелательным для нас образом, мы никогда об этом не узнаем. А поскольку мы уже используем модуль, в котором объявлен TOrderValidator, то мы можем потерять бдительность и начать использовать другие классы из этого модуля. И возможно, не успеем мы оглянуться, как наш код будет безнадёжно связан и с этими классами. Без надежды на освобождение. Крепко привязываться к другим классам – это плохая идея.
- Мы не можем знать, что делает или создаёт класс TOrderValidator. Он может создать тринадцать других классов, выделить огромный блок памяти, подключиться к базе данных и даже запустить ядерную ракету. Как знать? Побочным эффектом от простого создания экземпляра класса TOrderProcessor может быть всё что угодно. Что-то, что мы не в состоянии контролировать. И это что-то может меняться, ведь с другими частями кода могут работать другие члены команды.
- По тем же причинам, мы не можем знать все последствия от использования этих классов. Например, они могут делать изменения в production базе данных. TOrderValidator, например, при проверке может снимать с кредитной карточки компании $0.50. (А сейчас, представьте лицо вашего начальника, когда он получит выписку со счёта после пары тысяч прогонов наших модульных тестов с этим кодом…). В конце концов, мы тесно связаны с этими классами. И это плохо.
procedure DoOrderProcessing; var Order: TOrder; OrderProcessor: IOrderProcessor; OrderValidator: IOrderValidator; OrderEntry: IOrderEntry; begin GlobalContainer.Build; Order := TOrder.Create; try OrderValidator := ServiceLocator.GetService<IOrderValidator>; OrderEntry := ServiceLocator.GetService<IOrderEntry>; OrderProcessor := TOrderProcessor.Create(OrderValidator, OrderEntry); if OrderProcessor.ProcessOrder(Order) then begin WriteLn('Order successfully processed....'); end; finally Order.Free; end; end;
Обратите внимание, что мы всё ещё вызываем Create для TOrderProcessor. Но уже в следующей статье мы увидим как указать контейнеру, каким именно способом мы хотим создавать наш класс.
Таким образом, мы можем полностью отделить классы друг от друга и запрашивать реализации у контейнера. Если мы захотим, например, использовать mock-объекты для этих интерфейсов, мы сможем это сделать, просто зарегистрировав для этих интерфейсов mock-классы, вместо “реальных” классов.
Никогда не вызывай Create
Раз уж нам приходится быть настолько осторожными, чтобы не делать слишком много действий в конструкторе, так может, мы попробуем обойтись вообще без конструктора? Давайте представим, что у нас есть возможность инициализировать всё, что нужно, просто полагаясь на конструктор по умолчанию от TObject. Думаете, не получится? А вот и зря! Давайте сделаем это!
Должен отметить, что часто конструктор класса, всё же требуется для инициализации таких типов, как integer, string и других не-объектных типов. В этом случае, конструктор всё-таки понадобится. Здесь идея в том, чтобы показать, как можно разделить классы так, чтобы их даже не пришлось создавать.
Избавляемся от конструктора с помощью Field Injection
Внедрение полей (Field injection) это решение, с помощью которого можно избавиться от всех конструкторов сразу. Field injection даёт возможность внедрить реализацию зависимого класса прямо в ссылку поля объекта не вызывая конструктор вручную. Фреймворк Delphi Spring предоставляет два способа для внедрения полей.
Рассмотрим следующее объявление класса:
type TOrderProcessor = class(TInterfacedObject, IOrderProcessor) private [Injection] FOrderValidator: IOrderValidator; FOrderEntry: IOrderEntry; public function ProcessOrder(aOrder: TOrder): Boolean; end;
Во-первых, обратите внимание, что у класса нет конструктора. Он всё ещё содержит ссылки на два вышеупомянутых внутренних интерфейса, но формального конструктора здесь нет. Во вторых, обратите внимание на атрибут [Injection]. Это ключевой момент. Этот атрибут говорит Spring Framework-у – “Если ты встретишь такой идентификатор, обратись к Container-у и возьми у него реализацию для этого интерфейса и присвой её переменной”. Проще говоря, этот атрибут указывает Spring-у сделать всё за программиста, включая создание экземпляра класса реализующего интерфейс.
Таким образом, мы можем не иметь конструктора для класса TOrderProcessor, и при этом свободно использовать интерфейсы, объявленные в private полях:
function TOrderProcessor.ProcessOrder(aOrder: TOrder): Boolean; var OrderIsValid: Boolean; begin Result := False; OrderIsValid := FOrderValidator.ValidateOrder(aOrder); if OrderIsValid then begin Result := FOrderEntry.EnterOrderIntoDatabase(aOrder); end; {$IFDEF CONSOLEAPP} WriteLn('Order has been processed....'); {$ENDIF} end;
Вышеприведённый код отлично сработает, невзирая на тот факт, что в нём нигде явно не вызывается Create для создания классов реализующих эти интерфейсы.
Но подождите, скажет внимательный читатель, а что насчёт поля FOrderEntry? Ведь у него нет атрибута [Injection]! Давайте я расскажу, о чём я умолчал в вышеприведённом коде. Для FOrderEntry внедрение поля реализовано напрямую, как часть процесса регистрации интерфейса/класса. В секции initialization этого модуля, есть следующая строка, отвечающая за регистрацию:
initialization GlobalContainer.RegisterComponent<TOrderProcessor>.Implements<IOrderProcessor>.InjectField('FOrderEntry');
В самом конце строки, вы можете заметить вызов InjectField(‘FOrderEntry’), который говорит Spring-у то же самое что и атрибут [Injection]. Действительно, атрибут [Injection] вызывает тот же самый код для корректной инициализации поля. (Модуль целиком можно найти на BitBucket).
Мы всё-таки сделали это! Мы написали полезный класс, использующий два других класса, и при этом мы ничего явно не создавали и не породили ни одной зависимости. Чудесно, не так ли?
Заключение
Итак, теперь мы умеем писать классы, которые настолько не связаны друг с другом, что могут обойтись без создания экземпляров объектов, которые они будут использовать. Мы использовали внедрение полей (Field Injection) для того, чтобы автоматически получить ссылки на объекты, реализующие наши интерфейсы. Эти классы настолько не связаны, что нам достаточно включать модули, с реализацией классов только в *.DPR файл проекта, и совершенно незачем в uses часть наших других модулей. И как я уже упоминал в самом начале, “Отсутствие связей это хорошо, а наличие связей – плохо”. (Если я этого не говорил, то мне следовало это сказать, да?) (Примечание переводчика: большого труда мне стоило не подменить цитату Ника на Омара Хайяма: «Чтоб мудро жизнь прожить, знать надобно немало. Два важных правила запомни для начала: Ты лучше голодай, чем что попало есть, и лучше будь один, чем вместе с кем попало»).
DI - это интересно. Но... как-то, непривычно, что ли?
ОтветитьУдалитьВообще, развязывание кода - это хорошо. Сам уже давно дошёл до того, что в большом проекте есть один модуль, в котором собраны все (ну почти все) классы, которые на самом деле являются ссылками на классы, реализованные в других модулях. Таким образом, мне действительно не нужно вспоминать, а в каком это у меня модуле описан такой-то класс (ну и соответственно чтобы подменить один модуль другим, достаточно внести правку в одном месте).
Я у себя в проекте реализовал что-то типа IoC.
УдалитьЧтобы уменьшить зависимости, я у себя в проекте пару лет назад, перенёс большую часть глобальных переменных в модуль (даже в один класс). Все переменные из классов переделал в интерфейсы.
И все объявления вынес в один модуль в отдельный package (MY_CORE), а регистрация реализаций происходила в других модулях. Соответственно, все интерфейсы были объявлены в одном пакете (MY_CORE), а регистрация реализаций происходила в других пакетах.
Delphi Spring в этом плане выглядит более удобным решением.
p.s. Всё было бы идеально, если б после изменений в интерфейсах в MY_CORE, не приходилось перекомпилировать все зависящие от него пакеты. Хорошо, хоть, что есть скрипт для перекомпиляции всего в нужном порядке.
Восхитительно! Спасибо!
ОтветитьУдалитьЕсть ли примеры проектов открытым исходным кодом, где используется Delphi Spring?
Как по вашему, на сколько рискованно использовать Delphi Spring в продакшен проекте?
Дмитрий, пока что я только перевожу материал. Я даже не пробовал использовать Delphi Spring в деле. Если придётся делать новый проект и там это будет уместно, то обязательно попробую. На работе его внедрять пока не могу так как до сих пор большую часть времени работаю в неподдерживаемом Спрингом Delphi 6.
УдалитьНасчёт риска использования. Сомневаюсь, что риск сильно выше чем при использовании любой другой библиотеки. Ошибки случаются даже в платных библиотеках. А если рассматривать риск прекращения поддержки проекта, то исходники остаются и доступны.
Ник в своих постах писал, что на своей текущей работе они активно используют Delphi Spring в продакшене.