Это перевод публикации Ника Ходжеса: Getting Giddy with Dependency Injection and Delphi Spring #5 – Delphi Spring Basics.
Все переводы по 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. Один интерфейс несколько реализаций.
Вступительное слово
Я много слышал о фреймворке Spring для Java. И даже знал, что аналогичный фреймворк был создан и для Delphi. Но у меня не хватало терпения сесть и разобраться. Также, как и с терминами “Внедрение зависимости” (Dependency Injection) и “Обращение управления” (Inversion of Control). Я часто встречал упоминания о них в разных статьях, но так и не смог уложить в своей голове, как применить эти знания к Delphi. И вот, наконец, я наткнулся на публикацию Ника. То, что я прочитал в этой публикации, запросто расставило всё по своим местам. Это было настолько потрясающе, что я решил обязательно перевести этот материал и опубликовать перевод у себя в блоге. Ник дал добро, и процесс пошёл.
Переводить было легко и просто. Всё-таки чувствуется разница между переводом материала, написанного носителем языка, и материала, написанного человеком для которого английский - не родной. Помню, как я мучился с переводом материалов по дженерикам в Delphi, пытаясь уловить, что же хотел сказать автор. Периодически даже пытался уловить смысл, заглядывать в оригинал материала на французском языке. Представляю, каково приходится переводчикам, чья основная специальность - локализация компьютерных игр, и кому приходится переводить не только текст, но и загадки и стихи. Бррр!
На самом деле, это уже 5я часть в серии публикаций, посвящённых Dependency Injection в блоге Ника (полный список ищите по ссылке). Но первые четыре публикации просто подводят читателя к необходимости писать код, используя как можно меньше зависимостей между классами. Я не стал их переводить. На мой взгляд, там не так много много полезной и уникальной информации, чтобы тратить время на перевод. Пятая часть представляет собой совершенно уникальный материал, рассказывающий об основах использования Delphi Spring Framework.
Помните, что вы можете загрузить Delphi Spring Framework на Google Code.
Введение
Итак, мы наконец добрались до точки, где можем начать использовать фреймворк Delphi Spring. До этого момента мы видели, насколько важен Закон Деметры для дизайна хорошего кода (примечание переводчика: о законе Деметры можно почитать здесь). Мы увидели, насколько важно писать легкотестируемый код, благодаря уменьшению зависимостей между объектами, и насколько лучше писать код для интерфейсов и делать наши конструкторы простыми. Я наконец готов выполнить своё обещание и показать, как написать модуль, содержащий полезный код и пустую секцию interface.
Я надеюсь, что я делаю доброе дело, описывая как и почему вам стоит использовать фреймворк Внедрения Зависимостей (Dependency Injection). Я надеюсь, что когда эта работа будет закончена, вы поймёте как использовать Delphi Spring Framework. Однако, я далёк от совершенства в том, что касается мотивации и причин для использования Внедрения Зависимостей. Однако, не всё потеряно – я настоятельно рекомендую вам посмотреть выдающийся эпизод из DotNetRocks TV, где очень умный парень по имени James Kovacs объясняет все основы Внедрения Зависимостей (с очаровательным канадским акцентом) и почему их стоит использовать. Он даёт примеры на C#, но пусть это не смущает любителей Delphi – концепции там всё равно одни и те же.
Проблемы с “DI через параметры конструктора”.
В предыдущем эпизоде я говорил о двух правилах, которым стоит следовать:
- Всегда пишите код для интерфейса (прим. переводчика: а не для реализации).
- Следите, чтобы конструкторы были простыми.
Мы рассмотрели как создавать чистый и тестируемый код без лишних зависимостей, благодаря тому, что публичным остаётся только один интерфейс, и как создание простых конструкторов уменьшает зависимости одного класса от другого. Мы видели пример того, что я назвал “Внедрение зависимости через параметры конструктора” (в оригинале: DICP - Dependency Injection via Constructor Parameters), когда объекты не создают внутренние классы сами, а получают их через параметризированные конструкторы.
DICP позволяет нам создавать нужные сервисы (классы) вне класса, использующего их. Но этот подход всё равно требует, чтобы вы создавали определённые классы в определённых местах вашего кода. Невзирая на то, что у нас уже есть некоторый уровень обращения управления (Inversion of Control), мы всё ещё привязаны к конкретному классу. Давайте рассмотрим следующий код, основанный на нашем коде Печи для Пиццы (Pizza Oven) из предыдущей публикации:
var MyPizzaOven: TPizzaOven; MyPizza: TPizza; begin MyPizza := TPizza.Create; MyPizzaOven := TPizzaOven.Create(MyPizza); end;
Этот код лучше, чем если бы класс TPizza создавался внутри конструктора TPizzaOven. Но у нас всё ещё есть проблема из-за того, что классы TPizza и TPizzaOven связаны между собой. Класс TPizzaOven должен знать о том, как объявлен класс TPizza.
Даже если мы переопределим TPizza через интерфейс IPizza, нам всё равно нужно будет знать о том, как создать экземпляр TPizza для реализации интерфейса IPizza. И это означает, что по сравнению с тем, что было раньше, мы не сильно продвинемся в разрывании зависимостей – TPizzaOven всё ещё должна иметь представление о конкретной реализации TPizza. По сути, в приведённом выше коде на самом деле ничего не изменится, даже если мы объявим переменную MyPizza как IPizza вместо TPizza. Согласны?
Но что, если бы существовал способ получить реализацию интерфейса вообще ничего не зная о конкретной реализации? Это позволило бы действительно разорвать зависимости и отделить интерфейс от функциональности. Ах, если б только существовал написанный в Delphi фреймворк позволяющий делать такие вещи. Ах, если бы!..
Контейнер Spring
Ладно, я вас разыгрывал. Такая вещь существует. Контейнер Delphi Spring позволяет это сделать. Контейнер Внедрения Зависимости (Dependency Injection) – это класс, который содержит ссылки на интерфейсы и классы для их реализации. Вы можете зарегистрировать конкретную реализацию интерфейса в контейнере. Если вам нужна реализация интерфейса, вы запрашиваете её у контейнера, и он предоставляет ссылку на реализацию в виде указанного интерфейса. Вы ссылаетесь только на контейнер и, таким образом, избегаете прямой связи между классом, которому нужна реализация, и самой реализацией. Чудесно, не так ли?
Но как же это работает? Первым делом вы наверняка захотите создать реализацию контейнера Spring в виде одиночки. Я создал следующий модуль с названием uServiceLocator, который предоставляет доступ к контейнеру Spring в виде одиночки (синглтона/singleton).
unit uServiceLocator; interface uses Spring.DI; function ServiceLocator: TContainer; implementation var FContainer: TContainer; function ServiceLocator: TContainer; begin if FContainer = nil then begin FContainer := TContainer.Create; end; Result := FContainer; end; end.
Модуль uServiceLocator использует класс TContainer, который объявлен в модуле Spring.DI из Delphi Spring Framework. Я назвал свой класс “ServiceLocator”, потому что он производит локацию сервисов. Мы можем использовать этот модуль везде, где нам понадобится зарегистрировать классы и интерфейсы, а также везде где мы будем использовать эти зарегистрированные интерфейсы.
TContainer это могущественный класс, который позволяет нам зарегистрировать классы реализующие указанные интерфейсы, используя для этого преимущества дженериков и анонимных методов (примечание переводчика: объяснение анонимных методов в Delphi ищите здесь и здесь (на русском) или здесь (на английском)) . В следующих статьях я углублюсь в таинственные принципы внутренней работы TContainer, а сейчас мы просто рассмотрим практические вопросы касающиеся использования этого класса.
Регистрация в контейнере
Итак, если вы хотите использовать контейнер для того, чтобы зарегистрировать классы с интерфейсами, или для того, чтобы использовать определённый интерфейс, то всё что вам нужно для этого – использовать модуль uServiceLocator вместо всех модулей, в которых эти классы определены. Фактически, вы можете определить свои классы в implementation секции вашего модуля и оставить в секции interface только объявление интерфейса. Помните модуль uNormalMathService из прошлой публикации? Сейчас мы можем объявить его следующим образом:
unit uNormalMathService; interface type IMathService = interface ['{BFC7867C-6098-4744-9774-35E0A8FE1A1D}'] function Add(a, b: integer): integer; function Multiply(a, b: integer): integer; end; implementation uses uServiceLocator; type TNormalMathServiceImplemenation = class(TInterfacedObject, IMathService) function Add(a, b: integer): integer; function Multiply(a, b: integer): integer; end; { TNormalMathServiceImplemenation } function TNormalMathServiceImplemenation.Add(a, b: integer): integer; begin Result := a + b; end; function TNormalMathServiceImplemenation.Multiply(a, b: integer): integer; begin Result := a * b; end; procedure RegisterNormalMathService; begin ServiceLocator.RegisterComponent<TNormalMathServiceImplemenation>.Implements<IMathService>('Normal'); ServiceLocator.Build; end; initialization RegisterNormalMathService; end.
Во-первых, обратите внимание, что интерфейс IMathService – единственное, что доступно для использования извне этого модуля. Всё остальное спрятано в секции implementation. Очевидно, что самое ценное здесь – это вызов процедуры RegisterNormalMathService. Именно там регистрируется реализация IMathService. Эта процедура вызывается из секции initialization модуля, поэтому простое включение этого модуля в проект гарантирует, что TNormalAdditionService будет зарегистрирован и доступен для использования любому другому модулю, который использует ServiceLocator (примечание переводчика: На практике, если запрашивать реализацию IMathService у ServiceLocator-а из секции initialization другого модуля, может возникнуть ситуация, что процедура регистрации ещё не успеет сработать). Вызов ServiceLocator.Build гарантирует что TContainer готов обрабатывать запросы для получения реализации интерфейсов.
Итак, что же происходит в вызове RegisterNormalMathService? Это почти не требует объяснений. Код исполняется в основном так же как и читается: “Зарегистрировать класс называющийся TNormalMathServiceImplementation, который реализует интерфейс IMathServiceinterface под именем ‘Normal’”. (Вот что означает последний строковой параметр – именованная ссылка на интерфейс. Это позволит вам зарегистрировать несколько реализаций для одного и того же интерфейса и запрашивать их по имени).
Nota Bene: Если вы собираетесь предоставить несколько реализаций для данного интерфейса, то наверное будет лучше объявить этот интерфейс в отдельном модуле и включить этот модуль в uses в секции implementation и, таким образом, получить модуль, у которого в секции interface вообще не будет никакого кода. Видите, я же говорил что это сработает (примечание переводчика: это обещание Ник давал в предыдущих публикациях).
И как только сервис будет зарегистрирован, вы сможете использовать его без лишних зависимостей:
unit uCalculator; interface implementation uses uServiceLocator, uNormalMathService; type TCalculator = class private FMathService: IMathService; public constructor Create; function Addition(a, b: integer): integer; function Multiplication(a, b: integer): integer; end; constructor TCalculator.Create; begin FMathService := ServiceLocator.Resolve<IMathService>('Normal'); end; function TCalculator.Addition(a, b: integer): integer; begin Result := FMathService.Add(a, b); end; function TCalculator.Multiplication(a, b: integer): integer; begin Result := FMathService.Multiply(a, b); end; end.
В этом случае, самая значимая часть кода – это конструктор класса, где ServiceLocator просят найти и предоставить рабочий экземпляр IMathService, используя реализацию зарегистрированную с именем “Normal”. После этого Контейнер проходит по списку зарегистрированных элементов, находит подходящую реализацию, создаёт экземпляр этого класса, и возвращает его как интерфейс. Всё это происходит “авто-магически” внутри класса TContainer.
Позже мы посмотрим, как можно контролировать время жизни созданных классов, объединять их в пул, или создавать экземпляры-Одиночки. Вы даже сможете получить полный контроль над созданием результирующих классов благодаря широким возможностям анонимных методов.
Обратите внимание, что класс знает только о IMathService. Он использует модуль uNormalMathService, но реализация этого класса полностью скрыта. Единственная причина, почему модуль uNormalMathService находится в списке uses файлов в том, что именно в том модуле объявлен интерфейс IMathService. Если вы действительно хотите, вы можете сделать так, как было написано в блоке с цитатой чуть выше, и объявить интерфейс в отдельном модуле, и тогда вам вообще не придётся использовать модуль, который реализует IMathService.
Заключение
Итак, мы сделали простой пример использования Delphi Spring DI Контейнера, который полностью разрывает зависимость между классом и кодом, который использует этот класс. В приведённом выше примере, класс TCalculator ничего не знает и никак не связан с реализацией интерфейса IMathService. За связывание отвечает контейнер Delphi Spring.
Пока это всего лишь простая демонстрация базовых возможностей Delphi Spring Container-а. Фреймворк Delphi Spring на самом деле умеет легко и автоматически создавать реализации для интерфейсов без написания какого-либо кода вообще. Так что, оставайтесь на связи и следите за следующими публикациями.
Ссылки по теме (от переводчика):
Мода, мода. О сколько ты сгубила душ, и сколько сгубишь!
ОтветитьУдалитьИкона шаблонов проектирования пошатнулась под весом новой иконы. Dependency Injection - ее имя.
Один хорошиый человек дал метко определение всем этим модным штучкам - свистоперделки. Особенно ежели притащить это в delphi. Все эти внедрения зависимости по-сути есть реализация очередного велосипеда автозагрузчика. В скриптовых языках а-ля PHP в них еще есть смысл (когда говнокодеру лениво думать как все собрать ручками), но в настоящих компилируемых языках это вообще не нужно. Пример, кстати показательный. Сначала две строчки кода, как это можно сзелать по-простому. А потом аж два юнита бесполезного кода, который делает то же самое.
Дорогой аноним, а в коммерческих проектах от средних размеров ServiceLocator показывает себя превосходно, позволяя радикально уменьшить связность частей проекта вплоть до физического разделения. А тот, кто думает, что может все собрать ручками - говнокодер и есть.
ОтветитьУдалитьА я то дурень двадцать лет думал, что для разделения программы на функциональные части существуют библиотеки.
ОтветитьУдалить> Икона шаблонов проектирования пошатнулась под весом новой иконы. Dependency Injection - ее имя.
ОтветитьУдалитьDependency Injection - лишь украшение к этой иконе. Единственное, что пошатнулось - это позиции фабричных паттернов.
Я у себя в проекте, года два тому назад ввёл систему сервисов, а-ля ServiceLocator. Через интерфейсы кстати. Для того, чтобы было проще разорвать зависимости между модулями и разнести функционал по отдельным пакетам.
> А я то дурень двадцать лет думал, что для разделения программы на функциональные части существуют библиотеки.
Одно другому не мешает. Вот мой дедушка например, до сих пор думает, что для того чтобы переслать кому-нибудь деньги существуют почтовые переводы. А лично мне удобнее перечислять деньги через интернет-банк. Каждому своё. ;)
>Одно другому не мешает. Вот мой дедушка например, до сих пор думает, что для того чтобы переслать кому-нибудь деньги существуют почтовые переводы. А лично мне удобнее перечислять деньги через интернет-банк
ОтветитьУдалитьДедушка наличные переводит, а Вы цифирьки электронные. Наличные это универсальность, всеприменимость и скорость, а цифирьки это узкоспециализированная штука недоступная каждому.
Так и здесь.
Кто-нибудь это реально применял у себя на проектах?
ОтветитьУдалитьDelphi Spring не использовал. Сходу не разобрался, как его прикрутить. К тому же, так и не смог придумать, какие дополнительные бонусы я получу от внедрения Delphi Spring. Может дело в специфике проекта, а может в том, что не разобрался.
ОтветитьУдалитьКогда-то сам начал делать что-то подобное. Идею подкинул Snowy. Сделал в программе микроядро, с возможностью регистрации своих сервисов в виде интерфейсов. Сервисы примерно такие: сервис логирования, сервис отладки, сервис генератора отчётов, сервис настроек, сервис работы с БД, сервис сохранения конфигов. Таким образом, стало проще заменить одну реализацию другой. Это позволило вынести большую часть функционала в отдельные пакеты. Но до полноценной DI пока не дорос.