Внедрение зависимостей в. Внедрение зависимостей в .NET. Руководство по применению этого механизма. Net приложениях. Книга демонстрирует основные паттерны на обычном языке C#, поэтому вы в полной мере поймете, как работает механизм внедрения зависимостей
Скачать 5.66 Mb.
|
Featured Products<% foreach (var product in this.Model.Products) { %> <%= this.Html.Encode(product.SummaryText) %> <% } %> Заметьте, насколько чище листинг 2-4 по сравнению с листингом 2-3. Первое усовершенствование – это то, что больше не нужно приводить элемент словаря к последовательност и товаров до того, как итерация станет возможной. Я с легкостью выполнил это, разрешив странице Index.aspx наследоваться от System.Web.Mvc.ViewPage , а не от System.Web.Mvc.ViewPage . Это означает, что свойство Model страницы имеет тип FeaturedProductsViewModel Вся строка отображения товара берется прямо из свойства SummaryText этого товара. Оба усовершенствования связаны с введением моделей конкретных представлений, которые инкапсулируют поведение представления. Эти модели являются POCO- объектами (Plain Old CLR Objects). Рисунок 2-14 предоставляет обзор структуры таких объектов. Рисунок 2-14: FeaturedProductsViewModel содержит список ProductViewModels . И FeaturedProductsViewModel , и ProductViewModel являются POCO-объектами, что делает их в высокой степени доступными для модульного тестирования. Свойство SummaryText полученно из свойств Name и UnitPrice для того, чтобы инкапсулировать логику отображения. 60 Чтобы код из листинга 2-4 был рабочим, HomeController должен возвращать представление с экземпляром FeaturedProductsViewModel . Например, на первом шаге он может быть реализован внутри HomeController , как это продемонстрировано ниже: public ViewResult Index() { var vm = new FeaturedProductsViewModel(); return View(vm); } Это позволит веб-приложению выполняться без ошибок, но список рекомендуемых товаров всегда будет пустым. Предоставление списка рекомендуемых товаров – это задача доменной модели. Рисунок 2-15 демонстрирует текущее состояние реализации архитектуры, показанной на рисунке 2-2. Рисунок 2-15: На данном этапе реализован только уровень пользовательского интерфейса, осталось реализовать уровни доменной логики и доступа к данным. Сравним этот рисунок с рисунком 2-6, который демонстрирует успехи Мэри на этом же самом этапе. Одним из преимуществ того, что мы начинаем создание приложения с пользовательского интерфейса, является то, что мы уже получаем приложение, которое можно запускать и тестировать. Т олько на более позднем этапе, продемонстрированном на рисунке 2-8, Мэри приходит к тому моменту, когда она может запускать и тестировать свое приложение. 61 Несмотря на то, что пользовательский интерфейс теперь существует, он не представляет особого интереса. Список рекомендуемых товаров всегда пуст, поэтому мне нужно реализовать некоторую доменную логику, которая сможет обеспечить должный список товаров. Доменная модель Доменная модель – это обычная, несложная C# библиотека, которую я добавляю в свое решение. Эта библиотека будет содержать POCO-объекты и абстрактные типы. POCO- объекты будут моделировать домен, в то время, как абстрактные типы обеспечивают абстракции, которые будут выступать в роли моих основных внешних записей в доменной модели. Принцип программирования на основании интерфейсов, а не конкретных классов – это основа механизма внедрения зависимостей. Именно этот принцип позволяет нам заменять одну реализацию другой. Интерфейсы или абстрактные классы? Многие руководства по объектно-ориентированно му проектированию фокусируются на интерфейсах как на главном механизме абстракций, в то время, как руководства по проектированию на базе .NET Fram ework поддерживают превосходство абстрактных классов над интерфейсами. Следует ли вам использовать интерфейсы или же все-таки абстрактные классы? Относительно механизма внедрения зависимостей утешительный ответ на данный вопрос – не имеет значения, что вы будете использовать. Важно лишь то, что вы программируете относительно некоторого рода абстракции. Выбор между интерфейсами и абстрактными классами важен в других контекстах, но не в данном. Вы заметите, что я использую эти понятия взаимозаменяемо; я часто использую термин "абстракция", чтобы объединить оба этих термина "интерфейсы" и абстрактные классы. Я все еще руководствуюсь подходом "снаружи-внутрь", поэтому я на некоторое время добавлю код в уровень пользовательского интерфейса. Некоторый добавленный мной код 62 будет использовать типы из доменной модели. Это означает, что я добавлю ссылку на доменную модель из пользовательского интерфейса, как это делала Мэри. Все получится отлично, но я отложу анализ диаграммы зависимостей до раздела "Анализ слабо связанной реализации", чтобы я смог предоставить вам полную картину. Универсальная абстракция доступа к данным обеспечивается паттерном Repository, поэтому я определю абстрактный класс ProductRepository в библиотеке доменной модели. public abstract class ProductRepository { public abstract IEnumerable GetFeaturedProducts(); } Готовый Repository будет иметь больше методов для поиска и изменения товаров, но следуя принципу "снаружи-внутрь", я определяю только классы и члены, которые мне нужны для текущей задачи. Проще добавить функционально сть в код, чем удалить все. Класс Product также реализован с самым минимальным количеством членов, что проиллюстрировано на рисунке 2-16. Рисунок 2-16: Класс Product содержит только свойства Name и UnitPrice , поскольку это единственные свойства, которые нужны для реализации желаемой возможности приложения. ApplyDiscountFor применяет в случае необходимости предоставить скидку для пользователя и возвращает экземпляр класса DiscountedProduct . Абстрактный GetFeaturedProducts возвращает последовательность Products Метод Index HomeController должен использовать экземпляр ProductService для того, чтобы извлечь список рекомендуемых товаров, применить какую-либо скидку, 63 сконвертировать экземпляр ы Product в экземпляры ProductViewModel , затем добавить их в FeaturedProductsViewModel . Поскольку класс ProductService принимает в свой конструктор экземпляр ProductRepository , самое сложное – обеспечить его соответствующим экземпляром. Вспомните из анализа реализации Мэри, что создание зависимостей при помощи ключевого слова new является неправильным. Как только я это сделаю, я окажусь сильно связанным с типом, который я только что использовал. Я собираюсь отказаться от контроля над зависимостью ProductRepository . Как продемонстрировано в следующем листинге, я лучше буду полагаться на что-нибудь другое, чтобы получить экземпляр с помощью конструктора HomeController . Этот паттерн называется Constructor Injection (внедрение через конструктор) – как и кем создается экземпляр не является заботой HomeController Листинг 2-5: HomeController с паттерном Constructor Injection 1. public partial class HomeController : Controller 2. { 3. private readonly ProductRepository repository; 4. 5. public HomeController(ProductRepository repository) 6. { 7. if (repository == null) 8. { 9. throw new ArgumentNullException("repository"); 10. } 11. this.repository = repository; 12. } 13. 14. public ViewResult Index() 15. { 16. var productService = new ProductService(this.repository); 17. 18. var vm = new FeaturedProductsViewModel(); 19. 20. var products = productService.GetFeaturedProducts(this.User); 21. foreach (var product in products) 22. { 23. var productVM = new ProductViewModel(product); 24. vm.Products.Add(productVM); 25. } 26. return View(vm); 27. } 28. } Строка 5: Внедрение через конструктор Строка 11: Сохраняет внедренную зависимость для дальнейшего использования Строка 16: Передает внедренную зависимость Конструктор HomeController указывает, что каждый, кому захочется использовать класс, должен предоставить экземпляр ProductRepository (который, как вы, возможно, помните, является абстрактным классом). Ограждающее условие гарантирует выполнение путем генерирования исключения в случае, если используемый экземпляр пуст. Внедренная зависимость может быть сохранена и в дальнейшем безопасно использоваться другими членами класса HomeController 64 В первый раз, когда я услышал о внедрении через конструктор, мне понадобилось много времени на то, чтобы осознать реальную пользу этого паттерна. Не приведет ли использование такого подхода к тому, что вся тяжесть управления зависимостями ляжет на другой класс? Да, приведет – и в этом-то все дело. В n-уровневых приложениях мы можем вынести управление зависимостями любыми способами на самый верхний уровень приложения – в C om position Root. Это централизованное место, в котором могут компоноваться различные модули приложения. Это можно сделать вручную или же делегировать эту задачу DI-контейнеру. HomeController делегирует большую часть своей работы классу ProductService , продемонстрированному в следующем листинге. Класс ProductService соответствует классу Мэри с тем же именем, но на данный момент он является подлинным классом доменной модели. Листинг 2-6: Класс ProductService 1. public class ProductService 2. { 3. private readonly ProductRepository repository; 4. 5. public ProductService(ProductRepository repository) 6. { 7. if (repository == null) 8. { 9. throw new ArgumentNullException("repository"); 10. } 11. this.repository = repository; 12. } 13. public IEnumerable 14. 15. { 16. if (user == null) 17. { 18. throw new ArgumentNullException("user"); 19. } 20. 21. return from p in this.repository.GetFeaturedProducts() 22. select p.ApplyDiscountFor(user); 23. } 24. } Строка 5: И снова используем Constructor Injection Строка 14: Паттерн Method Injection Строка 21-22: Используем обе внедренные зависимости для реализации поведения Метод GetFeaturedProducts теперь принимает в качестве параметра экземпляр IPrincipal , который представляет собой текущего пользователя. Это еще одно отклонение от реализации Мэри, показанной в листинге 2-1, которая всего лишь принимала в качестве параметра булево значение, указывая, является ли пользователь привилегированным. Т ем не менее, поскольку решение о том, является ли пользователем привилегированным, – это часть доменной логики, корректнее будет явно смоделировать 65 текущего пользователя в виде зависимости. Мы должны всегда соблюдать принцип программирования на основании интерфейса, но в данном случае мне не нужно ничего изобретать (как я это делал с ProductRepository ), потому что в библиотеку стандартных классов .NET уже входит интерфейс IPrincipal , который представляет собой стандартный способ моделирования пользователей приложения. Передача зависимости в метод в качества параметра называется внедрением через параметры метода (M ethod Injection). И снова управление делегируется вызывающему оператору, подобно внедрению через конструктор. Несмотря на то, что детали могут варьироваться, основная технология остается той же. На данном этапе приложение все еще не работает. Осталось две проблемы: Отсутствует конкретная реализация ProductRepository . Эта проблема легко решается. В следующем разделе я буду реализовывать конкретный ProductRepository , который считывает рекомендуемые товары из базы данных. По умолчанию ASP.NET MVC предполагает, что контроллеры обладают конструкторами по умолчанию. Поскольку я ввел параметр в конструктор HomeController , MVC fram ework не знает, как создать экземпляр HomeController Эта проблема может быть решена путем создания пользовательского IControllerFactory . Т о, как это делается, выходит за рамки данной главы, но этот вопрос будет обсуждаться в главе 8. Достаточно просто сказать, что эта пользовательская фабрика создаст экземпляр конкретного ProductRepository и передаст его в конструктор HomeController В доменной модели я работаю только с типами, определенными в рамках доменной модели (и библиотеки стандартных классов. NET ). Сущности доменной модели реализуются в виде POCO-объектов. На данном этапе существует одна единственная представленная сущность, названная Product . Доменная модель должна уметь взаимодействоват ь с внешним миром (например, с базами данных). Эта необходимость смоделирована в виде абстрактных классов (например, Repositories ), которые мы должны заменить конкретными реализациями перед тем, как доменная модель станет полезной. Рисунок 2-17 демонстрирует текущее состояние реализации архитектуры, продемонстрированной на рисунке 2-2. Рисунок 2-17: Уровень пользовательского интерфейса и уровень доменной логики уже реализованы, остается реализовать уровень доступа к данным. Сравните этот рисунок с рисунком 2-7, который демонстрирует успехи Мэри на данном этапе. 66 Доменная модель приложения еще не является объектно-ориентированной; чтобы завершить цикл, мне нужно реализовать только один абстрактный ProductRepository Доступ к данны м Как и Мэри, мне хотелось бы реализовать мою библиотеку доступа к данным при помощи технологии LINQ to Entities, поэтому я последую тем же шагам, которые она выполняла в разделе "Создание сильно связанного приложения" при создании модели. Главное отличие – модель и CommerceObjectContext теперь являются всего лишь деталями реализации; но с помощью них я могу создать реализацию ProductRepository , что продемонстрировано в следующем листинге. Листинг 2-7: Реализация ProductRepository с помощью LINQ to Entit ies 1. public class SqlProductRepository : Domain.ProductRepository 2. { 3. private readonly CommerceObjectContext context; 4. 5. public SqlProductRepository(string connString) 6. { 7. this.context = new CommerceObjectContext(connString); 8. } 9. 10. public override IEnumerable 11. { 12. var products = (from p in this.context.Products 13. where p.IsFeatured 14. select p).AsEnumerable(); 15. return from p in products 16. select p.ToDomainProduct(); 17. } 18. } Строка 16: Конвертирует в Domain Product В приложении Мэри сгенерированная сущность Product использовалась в качестве доменного объекта, несмотря на то, что она была определена в базе данных. Т ак больше не происходит, потому что я уже определил класс Product в доменной модели. Когда я сгенерировал модуль, мастер создал для меня еще один класс Product и мне нужно выполнить конвертацию между этими классами. Рисунок 2-18 иллюстрирует, каким образом они определены в разных модулях. Существующий класс Product – это всего лишь деталь реализации, и я мог бы с легкостью сделать его внутренним, чтобы выразить его более явно. Рисунок 2-18: И библиотека доменной модели, и библиотека доступа к данным определяют класс под названием Product . Доменный класс Product – это важный класс, который инкапсулирует доменную сущность – товар. Класс Product библиотеки доступа к данным – это всего лишь искусственный объект мастера Entity Fram ework. Его можно легко переименовать или сделать внутренним. 67 Примечание Вы можете поспорить, что то, что Entity Fram ework не поддерживает неграмотные сущности, является его определенным дефектом (по крайней мере, не в версии .NET 3.5 SP1). Т ем не менее, это вид ограничения, с которым вы обязательно столкнетесь в реальных проектах программного обеспечения. Существующий Product задает конвертацию в доменный тип Product . Эта конвертация является обычным преобразованием соответствующих значений. Несмотря на то, что такая конвертация не относится конкретно к механизму внедрения зависимостей, я использую ее для более удобных измерений: Domain.Product p = new Domain.Product(); p.Name = this.Name; p.UnitPrice = this.UnitPrice; return p; После реализации SqlProductRepository я теперь могу настроить ASP.NET MVC для того, чтобы внедрить экземпляр SqlProductRepository в экземпляры HomeController Поскольку я более подробно буду рассматривать это в главе 7, я не буду демонстрировать это здесь. Рисунок 2-19 демонстрирует текущее состояние архитектуры приложения, продемонстрированной на рисунке 2-2. Рисунок 2-19: Все три уровня приложения на данный момент реализованы так, как это продемонстрировано на рисунке 2-2. Этот рисунок идентичен рисунку 2-2, но повторяется здесь для иллюстрации текущего состояния приложения. Рисунок также идентичен рисунку 2-8, который демонстрирует завершенное приложение Мэри. 68 Теперь, когда все корректно соединено вместе, я могу перейти к домашней странице приложения и получить такую же страницу, как продемонстрированная на рисунке 2-3. Анализ слабо связанной реализации Предыдущий раздел содержал множество подробной информации, поэтому вряд ли вас удивит то, что вы потеряли общее представление о происходящем. В этом разделе я попытаюсь объяснить то, что произошло, с помощью более ясных терминов. Взаимодействие Классы каждого уровня взаимодейству ют друг с другом напрямую или в абстрактной форме. Таким образом, они работают за пределами модулей, поэтому трудно будет проследить процесс их взаимодействия. Рисунок 2-20 иллюстрирует, как соединяются зависимости. Рисунок 2-20: Взаимодейст вие между элементами, включенными в механизм внедрения зависимостей в коммерческом приложении. Заметьте, что экземпляр SqlProductRepository внедряется в HomeController , а затем спустя некоторое время через HomeController внедряется в ProductService , который, в конечном счете, использует его. 69 При запуске приложения код файла Global.asax создает новую пользовательску ю фабрику контроллеров. Приложение сохраняет ссылку на фабрику контроллеров, поэтому, когда выполняется запрос страницы, приложение вызывает CreateController для фабрики. Фабрика ищет строку соединения в файле web.config и передает ее в новый экземпляр SqlProductRepository . Фабрика внедряет экземпляр SqlProductRepository в новый экземпляр HomeController , а затем возвращает этот экземпляр. Затем приложение вызывает метод Index для экземпляра HomeController , заставляя его создать новый экземпляр ProductService , передавая экземпляр SqlProductRepository в его конструктор. ProductService вызвает метод GetFeaturedProducts для экземпляра SqlProductRepository В конце концов, возвращается ViewResult с заполненным FeaturedProductsViewModel , а ASP.NET MVC фреймворк находит и отображает корректную страницу. Диаграмма зависимосте й В разделе "Анализ" мы увидели, как диаграмма зависимостей может помочь нам проанализировать и понять степень гибкости, которую обеспечивает архитектурная реализация. Изменил ли механизм внедрения зависимостей диаграмму зависимостей приложения? Рисунок 2-21 демонстрирует, что диаграмма зависимостей действительно изменилась. Доменная модель больше не имеет зависимостей и может выступать в роли автономного модуля. С другой стороны, у библиотеки доступа к данным теперь есть зависимость; в приложение Мэри у нее не было зависимостей. Рисунок 2-21: Диаграмма зависимостей, которая демонстрирует пример коммерческого приложения при использовании механизма внедрения зависимостей. Самое 70 примечательное отличие – это то, что у доменной библиотеки больше нет зависимостей. Серые блоки внутри черных блоков демонстрируют примерные классы каждой библиотеки для того, чтобы дать вам представление о том, где какие классы должны быть. В каждой библиотеке присутствует больше классов, чем продемонстрировано на этом рисунке. Это пробуждает наши надежды на то, что мы более плодотворно сможем ответить на первоначальные вопросы о композиции: Можем ли мы заменить пользовательский веб-интерфейс пользовательским интерфейсом WPF? Т акая замена была возможна до этого и остается возможной в рамках нового проектирования. Ни библиотека доменной модели, ни библиотека доступа к данным не зависят от пользовательского веб-интерфейса, поэтому мы можем с легкостью поместить на его место что-то другое. Можем ли мы заменить реляционную библиотеку доступа к данным библиотекой, которая работает с сервисом Azure Table Service? В главе 3 я буду описывать, как приложение размещает и создает экземпляры корректного ProductRepository , поэтому на данный момент примите следующее как данность: библиотека доступа к данным загружается с помощью позднего связывания, а имя типа определяется в виде настройки приложения в файле web.config . Можно удалить текущую библиотеку доступа к данным и внедрить новую библиотеку, поскольку она также предоставляет реализацию ProductRepository Нельзя больше использовать текущую библиотеку доступа к данным изолированно, поскольку она теперь зависит от доменной модели. Во многих видах приложений это не является проблемой, но если заинтересованные стороны захотят, чтобы такая возможность была реализована, я могу решить эту проблему путем добавления еще одного уровня абстракции: с помощью извлечения интерфейса из Product (скажем, IProduct ) и изменения ProductRepository для того, чтобы он работал с IProduct , а не с Product . Эти абстракции потом можно переместить в отдельную библиотеку, которая используется совместно библиотекой доступа к данным и доменной моделью. Это потребовало бы больших затрат времени, поскольку мне понадобилось бы писать код для преобразования Product в IProduct , но это все-таки возможно. 71 Благодаря проектированию на основании механизма внедрения зависимостей первоначальное веб-приложение может постепенно быть преобразовано в приложение Software + Services (доступ к программному обеспечению предоставляется заказчику через интернет) с богатым WPF интерфейсом и облачным движком хранилища данных. Единственное, что осталось от первоначальной работы – это доменная модель, но это целесообразно, поскольку доменная модель инкапсулирует все важные бизнес-правила, и по существу, нам следует ожидать, что она будет самым существенным модулем. При разработке приложений мы, вероятно, не можем предугадать каждое будущее направление, которое может нам понадобиться для того, чтобы получить товар, но это не проблема, поскольку мы можем хранить наши варианты открытыми для доступа. Механизм внедрения зависимостей помогает нам создавать слабо связанные приложения для того, чтобы мы могли заново использовать или заменять различные модули необходимыми модулями. 72 2.3. Р асширение шаблонного приложения Чтобы поддержат ь оставшуюся часть книги и полностью продемонстрировать различные аспекты механизма внедрения зависимостей, мне понадобится расширить шаблонное коммерческое приложение. До настоящего момента я сохранял приложение настолько простым и небольшим по размеру, насколько это было возможно, чтобы осторожно ввести некоторые центральные сущности и принципы. Поскольку одной из главных целей механизма внедрения зависимостей является управление сложностью, нам нужно сложное приложение, чтобы полностью оценить его силу. Я буду расширять приложение относительно двух аспектов: архитектурный рефакторинг и добавленные возможности. Архитектура До настоящего времени шаблонное приложение было трехуровневым приложением, но сейчас я хочу всунуть уровень презентационной модели между пользовательским интерфейсом и доменной моделью, как это показано на рисунке 2-22. Рисунок 2-22: Уровень презентационной модели вставляется в шаблонное приложение для того, чтобы отделить логику представления от центральной части приложения. Я переместил все контроллеры и модели представлений из уровня пользовательского интерфейса в уровень презентационной модели, оставляя в уровне пользовательского интерфейса только представления (файлы .aspx и .ascx ) и Composition Root . Главная причина такого перемещения – отделить Composition Root от логики представления; таким образом, я могу продемонстрировать вам различные варианты конфигурационных стилей, оставляя при этом неизменной настолько большую часть приложения, насколько это возможно. 73 "Скромный" объе кт Я разделяю приложение на уровень пользовательского интерфейса и уровень презентационной модели не только для образовательных целей; я регулярно проделываю это для всех разрабатываемых мной приложений, если они имеют пользовательский интерфейс. Т акое разделение обеспечивает понятную концепцию разделения логики представления (как ведут себя пользовательские интерфейсы) и логики отображения (как выглядят пользовательские интерфейсы). Благодаря такой концепции разделения вся логика помещается в уровень, где ее можно тестировать модульно, а вся разметка помещается в уровень, где может работать графический дизайнер, не боясь при этом что-то легко разрушить. Цель – получить настолько небольшой по размеру императивный код, насколько это возможно на уровне пользовательс кого интерфейса, поскольку я не собираюсь писать никаких модульных тестов для этого уровня. Ядро приложения, которое содержит только достаточный минимум кода для начальной загрузки самого себя, после чего оно делегирует всю остальную часть работы тестируемым модулям, называется скромным объектом (Hum ble object). В данном случае он содержит только представления и код начальной загрузки: Composition Root . Помимо этой архитектурной модификации я также хочу добавить более расширенную возможность, нежели та, что мы рассматривали до настоящего момента. В озмож ность добавления в корзину Список рекомендуемых товаров знакомит нас только с ограниченным уровнем сложности: в сценарий, доступный только для чтения, вовлечен единственный репозиторий. Следующий логичный шаг – ввести возможность добавления покупок в корзину. Рисунок 2-23 демонстрирует скриншот использования корзины покупок. Рисунок 2-23: Впечатляюще скудная по возможностям корзина покупок в реорганизованном коммерческом шаблонном приложении. 74 Чтобы обеспечить корзину покупок для каждого пользователя, мне понадобятся Basket , BasketRepository и хост поддерживаемых классов. Если вы похожи на меня, то вы захотите сначала увидеть класс Basket : рисунок 2-24 демонстрирует корзину и список ее элементов. Рисунок 2-24: Корзина и ее содержимое, которое представляет собой список Extent Extent представляет количество данного товара. С точки зрения механизма внедрения зависимостей классы Extent и Basket не особенно интересны: они оба являются POCO-классами без зависимостей. Больший интерес представляют классы BasketService и поддерживаемые классы, продемонстрированные на рисунке 2-25. Рисунок 2-25: BasketService и поддерживаемые классы. BasketService может извлекать и вычислять Basket для данного пользователя. BasketService использует BasketRepository для извлечения Basket и BasketDiscountPolicy с целью применения скидки (при необходимости) BasketService может использоваться для извлечения пользовательского Basket и применения скидок. Он использует абстрактный BasketRepository , чтобы получить содержимое Basket и абстрактного BasketDiscountPolicy для дальнейшего применения скидок. Обе эти абстракции внедряются в BasketService посредством технологии внедрения через конструктор. 75 public BasketService(BasketRepository repository, BasketDiscountPolicy discountPolicy) BasketDiscountPolicy может быть простой реализацией с жестко-закодиро ванной стратегией, например, предоставление привилегированным покупателям пятипроцентной скидки, как мы уже видели в этой главе. Данная стратегия реализуется с помощью DefaultProductDiscountPolicy , в то время как более сложная реализация через данные обеспечивается RepositoryBasketDiscountPolicy , который уже использует абстрактный DiscountRepository для получения списка уцененных товаров. Эта абстракция еще раз внедряется в RepositoryBasketDiscountPolicy посредством внедрения через конструктор: public RepositoryBasketDiscountPolicy(DiscountRepository repository) Для управления всем этим я могу воспользоваться BasketService , чтобы распределить операции над Basket : добавление элементов, а также отображение и очистка Basket . Для выполнения этого BasketService необходимы как BasketRepository , так и BasketDiscountPolicy , которые (как вы догадались) передаются в него через его конструктор: public BasketService(BasketRepository repository, BasketDiscountPolicy discountPolicy) Для дальнейшего усложнения мне необходим ASP.NET MVC контроллер с названием BasketController , содержащий интерфейс IBasketService , который я снова внедряю через конструктор: public BasketController(IBasketService basketService) Как показывает рисунок 2-25, класс BasketService реализует IBasketService , это и есть используемая нами реализация. BasketController , в конечном счете, создается при помощи пользовательского IControllerFactory , поэтому ей также потребуются эти абстракции. Если по пути вы потеряли направление наших мыслей, то рисунок 2-26 демонстрирует диаграмму, которая иллюстрирует то, как зависимости компонуются в окончательном приложении. Рисунок 2-26: Композиция шаблонного коммерческого приложения с добавленной возможностью отправки товаров в корзину, а также первоначальным списком рекомендуемых товаров на титульной странице. Каждый класс инкапсулирует свое содержание, и только Com position Root знает обо всех зависимостях. 76 Пользовательский IControllerFactory создает экземпляры BasketController и HomeController , предоставляя их вместе с соответствующими зависимостями. BasketService , например, использует переданный экземпляр BasketDiscountPolicy , чтобы применить стратегию скидок к корзине товаров: var discountedBasket = this.discountPolicy.Apply(b); Я не намекаю на то, что в данном случае переданный BasketDiscountPolicy – это экземпляр RepositoryBasketDiscountPolicy , который сам по себе является контейнером для DiscountRepository Это расширенное шаблонное приложение служит основой для большинства примеров кода оставшейся части книги. 77 2.4. Резюме Поразительно легко писать сильно связанный код. Даже когда Мэри намеревалась написать трехуровневое приложение, это вылилось большей частью в монолитный фрагмент "спагетти-кода" (говоря о многоуровневом представлении, мы называем его "лазанья"). Одной из множества причин того, что писать сильно связанный код так легко, является то, что и возможности языка, и наши инструменты уже ведут нас в этом направлении. Если нам нужен новый экземпляр объекта, мы можем воспользоваться ключевым словом new , а если у нас отсутствует ссылка на требуемую сборку, Visual Studio облегчает процесс ее добавления. Как бы то ни было, каждый раз при использовании ключевого слова new мы вводим сильное связывание. Самый лучший способ минимизации использования ключевого слова new – использование паттерна проектирования Внедрение через конструктор всюду, где нам нужен экземпляр зависимости. Второй пример главы продемонстрировал, как заново реализовать приложение Мэри, программируя на основании интерфейсов, а не конкретных классов. Внедрение через конструктор – это пример инверсии управления, поскольку мы инвертируем управление зависимостями. Вместо того чтобы создавать экземпляры при помощи ключевого слова new , мы делегируем эту ответственность стороннему компоненту. Как мы увидим из следующей главы, мы называем это место Com position Root. Это то место, в котором мы компонуем все слабо связанные классы приложения. 78 3. DI-контейнеры Меню: XML конфигурация Использование кода в качестве конфигурации Автоматическая регистрация Com position Root Паттерн Register Resolve Release Когда я был еще совсем ребенком, мы с мамой изредка готовили мороженое. Мы не делали это слишком часто, поскольку приготовление мороженого было трудоемким процессом, и очень сложно было получить в итоге настоящее мороженое. В случае если вы никогда не пробовали делать мороженое, рисунок 3-1 иллюстрирует процесс его приготовления. Рисунок 3-1: Приготовление мороженого – трудоемкий процесс, включающий множество возможностей для совершения ошибок 79 Настоящее мороженое делается на кремовой основе, которая представляет собой легкий заварной крем, приготовленный из сахара, яичных желтков и молока или крема. Если перегреть эту смесь, то она будет сворачиваться. Если даже вам удалось этого избежать, следующая фаза приготовления несет за собой еще больше проблем. Если оставить кремовую смесь в морозилке и не трогать ее, то она будет кристаллизоваться, поэтому вам придется регулярно ее помешивать до тех пор, пока она не станет максимально густой. Т олько после этого вы получите хорошее, приготовленное в домашних условиях мороженое. Несмотря на то, что приготовление мороженого – это медленный и трудоемкий процесс, если вы все же хотите его приготовить, и у вас есть все необходимые ингредиенты и оборудование, то вы можете воспользоваться выведенной мной технологией приготовления мороженого. Сегодня, около 30 лет спустя, моя тёща делает мороженое с периодичностью, несравнимой с той, с которой мы с мамой делали мороженое в более юные годы – не потому что она любит делать мороженое, но потому что она пользуется технологией, которая ей помогает. Т ехника остается все той же, но вместо того, чтобы постоянно доставать мороженое из морозилки и перемешиват ь его, она пользуется электрической мороженицей, которая выполняет это за нее (см. рисунок 3-2). Рисунок 3-2: Итальянская мороженица моей тещи 80 Механизм внедрения зависимостей – это первая и самая главная технология, но вы можете воспользоваться техническими средствами для облегчения процесса. В части 3 я буду описывать DI как технологию. Затем в части 4 мы рассмотрим технические средства, которые могут использоваться для поддержания технологии DI. Мы называем эти средства DI-контейнерами. В этой главе мы рассмотрим DI-контейнеры как сущность: как они вписываются во всеобщую картину механизма внедрения зависимостей, рассмотрим некоторые паттерны и технологии, предусматривающие их использование, а также рассмотрим некоторые исторические факты, касающиеся DI. Попутно мы рассмотрим некоторые примеры. Основной план-конспект главы проиллюстрирован на рисунке 3-3. Глава начинается с общего введения к DI-контейнерам, включая описание сущности под названием автоматическая интеграция, за которым следует раздел с описанием различных опций конфигурации. Вы можете читать о каждой из этих опций в отдельности, но думаю, было бы выгоднее, по крайней мере, прочитать о такой опции, как использование кода в качестве конфигурации, прежде чем читать об автоматической регистрации. Рисунок 3-3: Структура этой главы. Раздел "Перспектива DI-контейнеров" не является обязательным для прочтения 81 Центральный раздел этой главы – это мини-каталог паттернов проектирования, называемых DI-контейнеры. Несмотря на то, что этот раздел руководствуется форматом каталога, описание паттерна Register Resolve Release (RRR) опирается на описание паттерна Com position Root, поэтому имеет смысл читать их последовательно. Вы можете пропустить раздел об опциях конфигурации и перейти прямо к описанию паттернов, но эти разделы лучше читать по порядку. Последний раздел отличается от предыдущих. Он намного менее технический и фокусируется на том, как DI-контейнеры вписываются в экосистему .NET . Вы можете пропустить этот раздел, если вам не важен этот аспект. Цель главы – предоставить вам хорошее понимание того, что такое DI-контейнер, и как он сочетается со всеми остальными паттернами и принципами данной книги; в некотором смысле, вы можете рассматривать эту главу как введение к части 4 данной книги. В ней мы будем говорить о DI-контейнерах в широком смысле, тогда как в части 4 мы будет говорить о конкретных контейнерах и их API. Может показаться несколько странным то, что мы ведем разговор о DI-контейнерах здесь, в главе 3, а потом более или менее забываем о них на протяжении следующих шести глав, но для этого есть причина. В этой части книги я хочу обрисовать огромную картину механизма внедрения зависимостей, и важно, чтобы вы понимали, как DI-контейнеры 82 вписываются в эту схему. В части 2 и 3 я изредка буду демонстрировать вам некоторые примеры, в которые входит DI-контейнер, но в большинстве случаев я, в основном, буду придерживаться рассуждений. Принципы и паттерны, описанные в середине книги, могут быть применимы ко всем DI-контейнерам. 3.1. Знакомство с DI-контейнерами 3.2. Конфигурирование DI-контейнеров 3.3. Паттерны DI-контейнеров 3.4. Перспектива DI-контейнеров 3.5. Р езюме 83 3.1. Знакомство с DI-контейнерами DI-контейнер – это библиотека программных средств, которая может автоматизировать множество задач, включая компоновку объектов и управление их жизненным циклом. Несмотря на то, что можно написать весь необходимый код инфраструктуры с помощью Poor man's DI, такой способ не добавляет большой значимости приложению. С другой стороны, задача компоновки объектов является всеобщей по своей натуре и может быть решена в один момент и сразу для всех; это так называемый |