Внедрение зависимостей в. Внедрение зависимостей в .NET. Руководство по применению этого механизма. Net приложениях. Книга демонстрирует основные паттерны на обычном языке C#, поэтому вы в полной мере поймете, как работает механизм внедрения зависимостей
Скачать 5.66 Mb.
|
Как мы подготавливаем пул? Создаем ли мы все объекты в пуле заранее или наполняем его постепенно по мере поступления запросов? Для того чтобы заполнить пул заранее, необходимо, чтобы мы знали, по крайней мере, первоначальный размер пула. Возможно это тоже затратная операция, поскольку целью пула является обеспечение легкого доступа к объектам, требующим больших затрат. Но преимущество такого подхода заключается в том, что объекты становятся доступными для быстрого доступа. Может быть, есть возможность предварительно заполнить пул из исходного потока таким образом, чтобы он мог начать быть полезным объектам во время его наполнения. В противном случае мы можем начать с пустого пула и заполнять его постепенно по необходимости. Это приводит к тому, что в начале время доступа замедляется, но такой подход может помочь хранить пул в нужном размере. Минимальный размер Мы можем решить проблему подготовки пула путем ввода настраиваемого минимального размера. Если мы задаем минимальный размер, отличающийся от нуля, то перед тем, как объекты смогут его использовать, пул должен заполниться до этого размера. С другой стороны, при минимальном размере, равном нулю, объекты могут начать использовать его, как только он начнет заполняться. Максимальный размер Каков максимальный размер пула? Поведение пула при достижении максимального размера Что происходит, когда мы достигаем максимального размера пула? Позволяем ли мы пулу вырасти в размере? Если мы позволим ему это сделать, то мы рискуем попасть в ситуацию, когда у нас закончится память. Если не позволим, то, как, в таком случае, мы должны обрабатывать дополнительные запросы объектов? Один из вариантов – заблокировать вызов до тех пор, пока объект не станет доступным. Но если мы это сделаем, то мы должны, по крайней мере, предоставить вызывающему оператору возможность указания задержки. Еще один вариант – незамедлительно выдавать исключение. Очистка пула Сохраняем ли мы пул заполненным до того момента, как приложение начнет закрываться, или же мы начинаем очищать его, если замечаем, что он начинает превышать свой размер? 316 В таблице представлены все важные концепции, касающиеся пулов объектов. Но как в случае со стилем существования Web Request Cont ext нам не следует разрабатывать свои собственные пользовательские пулы объектов, а следует использовать те, которые предоставляются DI-контейнерами. П римечание Не все DI-контейнеры предоставляют стиль существования Pooled, поэтому очевидно, что мы можем выбрать этот стиль существования только, если он поддерживается нашим DI- контейнером. При использовании стиля существования Pooled, предоставляемого DI-контейнером, все варианты, описанные в таблице 8-3, могут быть недоступны. Нам придется работать только с тем, что доступно. Когда использовать стиль существования Pooled Стиль существования Pooled вступает в игру в тех ситуациях, когда у нас есть специфичные компоненты, часто используемые, но для создания которых необходимы большие затраты. Даже если создание компонента трудозатратно, нам все еще стоит предпочесть стиль существования Singleton, если это возможно, поскольку он позволяет нам работать с единственным экземпляром и расплачиваться за создание объекта всего лишь единожды. Из этого следует, что организация пула применима только тогда, когда рассматриваемый компонент не должен использоваться коллективно, что часто происходит в тех случаях, когда данный компонент не является потоко-безопасным. Если мы заглянем в веб- приложение, то стиль существования W eb Request Context мог бы стать разумной альтернативой; мы должны были понять, что стиль существования Pooled используется вне веб-приложений. Обратите внимание на то, что тот факт, что рассматриваемый компонент можно использовать повторно, является необходимым условием. Если этот компонент обладает обычным жизненным циклом, который исключает возможность повторного использования, то мы не можем организовать его пул. Одним из примером этого является W CF интерфейс ICommunicationObject , который имеет вполне определенный жизненный цикл. Когда ICommunicationObject находится в состоянии Closed или Faulted , то он может, по определению, никогда не покидать это состояние. Такой тип объекта не пригоден для организации пула. Мы должны уметь возвращать объект обратно в пул в первоначальном состоянии. П ример: Повторное использование затратны х репозиториев Однажды я работал над проектом, в котором нам нужно было из .NET кода взаимодейст вовать с мэйнфреймом. Ранее консультанты создали неуправляему ю COM библиотеку, которая могла взаимодейст вовать с некоторой конечной точкой мейнфрейма, и мы решили обернуть эту библиотеку в управляемую сборку. COM библиотека взаимодействовала с мейнфреймом посредством секретного протокола сетевых соединений. Чтобы его использовать, нам приходилось открывать соединение и проходить через квитирование установления связи. После открытия соединения мы могли 317 передавать сообщения с умеренной скоростью, но открытие соединения занимало много времени. Давайте посмотрим, как создать пул экземпляров ProductRepository , которые могут взаимодейст вовать посредством такого протокола. В проекте, в который я был вовлечен, мы называли COM библиотеку Xfer (видовой), поэтому давайте создадим пул экземпляров XferProductRepository П римечание Как и в примере стиля существования W eb Request Context, я не жду, что вы будете создавать пользовательские системы управления жизненным циклов объектов в пуле. Несмотря на то, что для управления пулами объектов вам следует использовать соответствующий DI-контейнер, я хочу вам показать упрощенный пример для того, чтобы продемонстрироват ь, как это работает. П редупреждение Следующий пример не является потоко-безопасным. Я убрал из него код синхронизации, чтобы сохранить в примере разумный уровень сложности, а потоко-безопасную реализацию я оставляю читателям для самостоятельной практики (всегда хотел так написать). Данный пример является еще одной вариацией ICommerceServiceContainer , несколько вариаций которого вы уже видели в этой главе. В следующем листинге продемонстрирована база контейнера. Листинг 8-12: Схема базы для организации пула контейнера public partial class PooledContainer : ICommerceServiceContainer { private readonly IContractMapper mapper; private readonly List { this.mapper = new ContractMapper(); this.free = new List } public int MaxSize { get; set; } public bool HasExcessCapacity { get { return this.free.Count + this.used.Count < this.MaxSize; } } } Несмотря на то, что вы планируете организовать пул экземпляров XferProductRepository , вы все равно конфигурируете ContractMapper в виде Singleton, поскольку он является сервисом, несохраняющим свое состояние. Для отслеживания процесса организации пула вы используете две коллекции: в одной из них хранятся доступные репозитории, а другая содержит репозитории, которые 318 используются на текущий момент. При создании и освобождении компонентов вы перемещаете репозитории между этими двумя коллекциями. Свойство MaxSize позволяет вам определить максимальный размер пула, а свойство HasExcessCapacity по существу является инкапсулированным вычислением, которое вы можете использовать в условных выражениях для определения того, превышаете ли вы по-прежнему размер пула. При такой вариации пула вы будете наполнять пул постепенно, по мере поступления запросов до тех пор, пока не превысите максимальный размер. Как демонстрирует следующий листинг, вы выдаете исключение, если превышаете максимальный размер и все еще получаете запросы. Листинг 8-13: Разрешение репозиториев из пула 1. public IProductManagementService ResolveProductManagementService() 2. { 3. XferProductRepository repository = null; 4. if (this.free.Count > 0) 5. { 6. repository = this.free[0]; 7. this.used.Add(repository); 8. this.free.Remove(repository); 9. } 10. if (repository != null) 11. { 12. return this.ResolveWith(repository); 13. } 14. if (!this.HasExcessCapacity) 15. { 16. throw new InvalidOperationException( 17. "The pool is full."); 18. } 19. repository = new XferProductRepository(); 20. this.used.Add(repository); 21. return this.ResolveWith(repository); 22. } 23. private IProductManagementService ResolveWith( 24. ProductRepository repository) 25. { 26. return new ProductManagementService(repository, 27. this.mapper); 28. } Строка 4-9: При возможности выбирает из пула Строка 10-13: Возвращает из пула Строка 19-20: Добавляет новый репозиторий Для того чтобы разрешить экземпляр IProductManagementService , вы начинаете работу с проверки того, доступен ли повторно используемый репозиторий. Если он доступен, то вы выбираете репозиторий из коллекции свободных репозиториев и перемещаете его в список используемых. Если вы смогли найти повторно используемый репозиторий, то вы можете незамедлит ельно вернуть сервис. 319 Если вы не можете найти доступный репозиторий в пуле, то существует две возможные причины такой ситуации: пул полностью заполнен и используются все репозитории, или вы все еще заполняете пул. Если вы можете обойти граничный оператор, который выполняет проверку заполненности пула, то вы создаете новый экземпляр репозитория и добавляете его в коллекцию используемых репозиториев перед тем, как вернуть скомпонованный сервис. ResolveProductManagementService всего лишь перемещает репозитории из коллекции free в коллекцию used , поэтому важно освободить сервисы после использования. В следующем листинге продемонстрировано, как это сделать. Листинг 8-14: Возврат репозиториев в пул 1. public void Release(object instance) 2. { 3. var service = instance as ProductManagementService; 4. if (service == null) 5. { 6. return; 7. } 8. var repository = service.Repository 9. as XferProductRepository; 10. if (repository == null) 11. { 12. return; 13. } 14. this.used.Remove(repository); 15. this.free.Add(repository); 16. } Строка 4-7, 10-13: Граничные операторы Строка 14-15: Возвращает репозиторий в пул Вернуть репозиторий в пул легко: вы перемещаете его из коллекции используемых репозиториев в коллекцию доступных репозиториев. Обратите внимание на то, что, даже если этот пример может показаться сложным, я не затронул несколько моментов: Пример определенно не является потоко-безопасным. Рабочая реализация должна позволять нескольким потокам разрешать и освобождать экземпляры параллельно. Поскольку класс XferProductRepository инкапсулирует неуправляемый код, он реализует IDisposable . Пока вы продолжаете повторно использовать экземпляры, вам не нужно их уничтожать, но вам точно следует это сделать в тех ситуациях, когда контейнер выходит за рамки области применения. Т аким образом, сам контейнер должен реализовывать IDisposable и удалять все репозитории с помощью метода Dispose Организация пула объектов – хорошо известный паттерн проектирования, но он часто инкапсулируется в существующие API; например, ADO.NET использует пулы соединений, но мы не сталкиваемся с ним явно. Только когда нам точно нужно оптимизировать доступ к затратным ресурсам, стиль существования Pooled начинает иметь смысл. 320 Стиль существования Pooled помогает справиться с ситуацией, когда нам нужно оптимизировать использование затратных ресурсов. Это самый последний из универсальных типов стилей существования. Другие стили существования Т ипы стилей существования, рассматриваемые в данной главе, являются самыми универсальными типами, но ваши нужды могут быть более экзотичными, и рассматриваемые стили могут не удовлетворять вашим нуждам. Когда я попал в такую ситуацию, первой моей реакцией было чувство чрезвычайной гордости от того, что я обнаружил редкий и очень краеугольный случай, который требовал от меня использования экзотического элемента из моего программистского инструментария. Следующей моей реакцией было осознание того, что мой подход абсолютно неверен, и если я слегка изменю свою модель, то все можно будет вполне подогнать к стандартным паттернам. Т акая реализация часто является ухудшением, но приводит к лучшему и более легко сопровождаемому коду. Суть заключается в том, что если вы чувствуете необходимость реализовать пользовательский стиль существования, то, прежде всего, вам необходимо со всей серьезность пересмотреть вашу модель. Было сказано, что некоторые DI-контейнеры предоставляют возможности расширяемости, которые позволяют вам разрабатывать пользовательские стили существования. Давайте вкратце рассмотрим два, технически возможных, но довольно экзотичных стиля существования. В обоих случаях я предоставляю только краткую схему того, как работал бы жизненный цикл. Я не отдаю под это целые разделы, поскольку мне было бы трудно придумать разумный сценарий их применения. Lazy Стиль существования Lazy или Delayed – это Virtual Proxy более затратной зависимости. Смысл в том, что если у нас есть требующая больших затрат зависимость, которую мы не планируем часто использоват ь, то мы можем отложить создание затратной зависимости до тех пор, пока она нам не понадобится. Рисунок 8-13 иллюстрирует, как можно внедрить потребителя с легковесным дублером для существующей, более затратной реализации. Рисунок 8-13: Потребителю необходима зависимость IService , но если он использует эту зависимость лишь в небольших фракциях своего жизненного цикла, то он может долгое время просуществоват ь до того момента, когда ему понадобятся сервисы IService Когда он, в конце концов, вызывает IService.SelectItem() , LazyService использует его внедренный IServiceFactory для создания экземпляра другого IService . К данному моменту ExpensiveService еще не создан. При создании ExpensiveService все последующие вызовы могут быть делегированы ему. 321 Имеет смысл использовать такой стиль существования только, если потребитель использует затратную зависимость лишь в небольших фракциях своего собственного жизненного цикла, или если мы можем трезво предположить, что до момента вызова зависимости пройдет значительное количество времени. Если зависимость вызывается незамедлительно или часто, то Lazy Decorator ни на что не влияет, но использует дополнительные ресурсы. Если это возможно, требующая больших затрат зависимость должна регистрироваться как Singleton, чтобы нам нужно было расплачиваться за ее создание лишь единожды. Если это невозможно по причинам потоко-безопасности, мы часто можем разрешить эту головоломку путем организации пула затратного компонента. Даже если чаще всего у нас может быть только один экземпляр, пул этого экземпляра в сочетании с задержкой доступа предоставит нам сериализованный доступ к зависимости. Стиль существования Lazy более интересен с технической точки зрения, нежели практически полезная стратегия жизненного цикла; если вам интересно, то я даю ссылки на рекомендуемую литературу, связанную с данной книгой. (M ark Seemann, "Rebuttal: Constructor over-injection ant i-pat tern," 2010, http://blog.ploeh.dk/2010/01/20/RebuttalConstructorOver inje ctionAntipattern.aspx ) Future Стиль существования Future даже более экзотичен. Смысл в том, что мы можем захотеть использовать зависимость, которая недоступна на данный момент, но которую мы будем использовать, когда она станет доступной. Наилучший способ реализации такого стиля существования похож на стиль существования Lazy: мы можем использовать Decorator, который делегирует полномочия первоначальной реализации до тех пор, пока нужная зависимость не станет доступной. Рисунок 8-14 иллюстрирует концептуальное взаимодействие между компонентами. Первоначальная реализация, используемая в качестве дублера до тех пор, пока Future Decorator не дождется нужной зависимости, часто является приложением паттерна проектирования Null Object. 322 Рисунок 8-14: Потребителю необходима зависимость IService , но DesiredService может быть еще недоступным. В этом случае мы можем инкапсулировать NullService в виде дублера, который будет использоваться до тех пор, пока мы находимся "в ожидании Годо". FutureService – это установленная машина, которая выполняет опрос с целью определения того, стал ли доступным DesiredService . Когда DesiredService недоступен, FutureService Decorator ничего не остается, как использовать резервную реализацию, обеспечиваемую NullService . Когда DesiredService становится окончательно доступным, все последующие запросы направляются к нему. Я должен согласиться, что я с трудом выжал из себя разумный пример того, когда зависимость может стать доступной после того, как мы присоединили полноценную диаграмму объектов. Это может казаться слегка похожим на ситуацию, когда мы полагаемся на внешний ресурс, например, базу данных или веб-сервис, но не забывайте, что даже если действующий ресурс недоступен, все еще существует программная зависимость; например, веб-сервис может не работать, но WCF прокси, который мы используем для взаимодействия с этим веб-сервисом все еще доступен. Лучше мы будем иметь дело с недоступными внешними ресурсами, использующими паттерн Circuit Breaker, который мы рассмотрим в следующей главе. До тех пор, пока кто- нибудь не предоставит мне разумный сценарий, я считаю стратегию жизненного цикла Future как технически интересную. На данный момент мы рассмотрели широкий спектр доступных стилей существования зависимостей – от универсальных до более экзотичных. 323 8.4. Резюме Когда мы применяем инверсию управления к зависимостям, мы инвертируем управление не только над выбором типа, но также над процессом управления жизненным циклом. Когда потребитель больше не создает свои собственные зависимости, он уже не может постановить, когда была создана зависимость, и была ли она разделена с другими потребителями. Composer 'ы могут принять решение, позволяющее множеству потребителей совместно использовать один и тот же экземпляр, или позволяющие каждому потребителю обладать своим собственным экземпляром. Кроме того, в игру вступают более продвинутые стратегии. Несмотря на то, что Composer 'ы отлично управляют тем, когда создаются объекты, управляемая модель памяти .NET подразумевает, что в большинстве случаев они лишь слегка влияют на то, когда разрушаются объекты. Зависимости могут выйти за рамки области применения и быть утилизированы с помощью сборщика мусора. Но особое место отведено компонентам, которые также реализуют IDisposable , поскольку мы должны убедиться, что все неуправляемые ресурсы очищены – иначе наши приложения вскоре начнут испытывать утечку памяти. Равносильно вызову метода Resolve (или какое название он имеет) мы должны всегда не забывать вызывать метод Release , когда разрешенная диаграмма объектов выходит за рамки области применения. Это дает Composer возможность уничтожить любые устраняемые компоненты, которые становятся неиспользуемыми. Каждая диаграмма объектов может иметь смесь множества различных стилей существования, и нам, кроме того, нужно следить за тем, являются ли компоненты устраняемыми. Добавьте к этой смеси потоко-безопасность, и станет сложно отслеживат ь все эти вещи. Именно здесь и расцветает только что распустившийся DI-контейнер, и это одна из причин того, что нам следует использовать DI-контейнер вместо Poor Man's DI. Каждый из множества доступных DI-контейнеров предлагает свой собственный доступный стиль существования. Некоторые из них поддерживают только несколько стилей существования, другие DI-контейнеры поддерживают большинство или все стили существования, но многие из них предоставляют возможности расширяемости, которые позволяют нам реализовывать свои собственные стили существования. Самый безопасный стиль существования – T ransient, так как экземпляры не делятся с кем- либо еще. Т акже это самый неэффективный стиль существования, поскольку, скорее всего, в оперативной памяти находится множество экземпляров одного и того же типа. Наиболее эффективный стиль существования – Singleton, потому что в оперативной памяти находится всего один единственный экземпляр (то есть на один контейнер). Но для этого стиля необходимо, чтобы компонент был потоко-безопасным, поэтому не всегда есть возможность использовать данный стиль существования. Стили существования Web Request Cont ext и Pooled обеспечивают хорошую альтернативу Singleton и T ransient , но в более ограниченных сценариях. 324 Возможны и более экзотичные стили существования. Стиль существования Future может с первого взгляда казаться отличным способом управления недоступными ресурсами, но как вы увидите в следующей главе, лучше мы обратимся к такой теме, как механизм перехвата. 325 9. Механизм перехвата Меню: Сквозные сущности Аспектно-ориентиро ванное программирование Динамический перехват В кулинарии одно из самых интересных – это то, как мы можем смешивать между собой множество ингредиентов; некоторые из них сами по себе не очень вкусные, а их смесь намного вкуснее. Часто мы начинаем с простого ингредиента, составляющего основу блюда, а затем изменяем и приукрашиваем его до тех пор, пока, в конечном итоге, не получим превосходное блюдо. Представьте себе говяжью отбивную. Если бы мы находились в полном отчаянии, то могли бы съесть ее сырой, но в большинстве случаев мы бы предпочли ее пожарить. Т ем не менее, если мы просто шлепнем ее на горячую сковороду, то результат будет менее выдающимся. За исключением подгоревшего привкуса вы больше ничего не почувствуете. К счастью, есть множество шагов, которые мы можем предпринять, чтобы повысить свою квалификацию: Жарка отбивной в масле предотвратит мясо от подгорания, но вкус все равно останется слабым. Добавление соли усилит вкус мяса. Добавление других специй, например, перца, сделает вкус мяса более богатым. Панировка отбивной в смеси из соли и специй не только добавит ей вкусовых качеств, но также придаст первоначальному ингредиенту новую структуру. На данном этапе мы приближаемся к тому, что называется котлетой. Прорезание в отбивной кармашка и добавление в него ветчины, сыра и чеснока перед панировкой поднимет нас на пьедестал почета. Мы получили кордон блю, самое превосходное блюдо. Разница между подгоревшей отбивной и кордон блю значительна, но исходный ингредиент этих блюд один и тот же. Вариативность вызвана теми ингредиентами, которые мы добавляем к первоначальному. Подав говяжью отбивную, мы можем приукрасить ее, не изменяя при этом главный ингредиент, и создать другое блюдо. С помощью слабого связывания мы можем выполнить аналогичный трюк при разработке программного обеспечения. Когда мы программируем на основе интерфейсов, мы можем преобразовать или улучшить основную реализацию, завернув ее в другие реализации данного интерфейса. Вы уже видели часть этой методики в действии в разделе 8.3.6 "Другие стили существования", где мы использовали ее для изменения жизненного цикла затратной зависимости, обертывая ее в Proxy. Этот подход может быть обобщен, что предоставляет нам возможность перехватить запрос потребителя к сервису; именно это мы и будем рассматривать в данной главе. Подобно говяжьей отбивной мы начинаем с основного ингредиента и добавляем больше ингредиентов для его улучшения, но, не меняем при этом его первоначальную суть. Механизм перехвата – одна из самых мощных возможностей, полученных нами благодаря 326 слабому связыванию. Он позволяет нам с легкостью применять принцип единичной ответственности и концепцию разделения. В предыдущих главах мы потратили множество усилий на перебрасывание нашего кода на ту позицию, где он действительно слабо связан. В данной главе мы начнем пожинать преимущества данного вложения. Рисунок 9-1 демонстрирует обзор структуры данной главы. После окончания изучения главы вы должны уметь использовать механизм перехвата для разработки слабо связанного кода согласно установленным принципам объектно-ориентированного проектирования. В частности вы должны приобрести способность успешно соблюдать концепцию разделения и применять сквозные сущности, при этом сохраняя код в отличном состоянии. Рисунок 9-1: Вся структура данной главы достаточно линейна. Мы начнем с ознакомления с механизмом перехвата, включая пример. Далее мы перейдем к разговору о сквозных сущностях. Данный раздел главы не обременен теорией, но наполнен примерами, поэтому, если вы уже знакомы с данной темой, вы можете перейти прямо к последнему разделу, касающемуся аспектов. Этот раздел является кульминацион ной точкой главы, так как знакомит вас с усовершенствованной и, кроме того, гибкой концепцией механизма перехвата. Поскольку понять, как работает механизм перехвата, не сложно, мы начнем с небольшого примера для того, чтобы определить контекст. Для того чтобы полноценно оценить возможности, мы должны изучить некоторые связанные понятия, например, аспект но- ориент ированное программирование (AOP) и SOLID принципы, и связать их с механизмом перехвата с помощью примеров. В конечном счете, вы увидите, как можно использовать DI-контейнер для того, чтобы обобщить механизм перехвата и облегчить его применение. Поскольку в основе механизма перехвата лежат хорошо известные паттерны проектирования и принципы объектно-ориентированного проектирования, данная глава наполнена различными примерами. Маршрут главы вполне прямолинеен, начинается с вводного примера и вырастает до более сложных понятий и примеров. Последнее и более продвинутое понятие можно быстро объяснить с помощью абстракции, но, поскольку это понятие, скорее всего, будет вам понятно только на примере, глава завершается примером, занимающим несколько страниц, который демонстрирует, как все работает. Но прежде чем мы до этого доберемся, мы должны начать с самого начала. 9.1. Знакомство с механизмом перехвата 9.2. Реализация сквозных сущностей 9.3. Объявление аспектов 9.4. Резюме 327 9.1. Знакомство с механизмом перехвата Концепция механизма перехвата довольно проста: нам хотелось бы уметь перехватывать сигнал между потребителем и сервисом, и исполнять некоторый код до или после вызова данного сервиса. На рисунке 9-2 обычный сигнал от потребителя к сервису перехватывается посредником, который может исполнить свой собственный код до или после передачи сигнала к фактическому сервису. Рисунок 9-2: Механизм перехвата в двух словах. Мы можем сконвертировать простой сигнал от потребителя к сервису в более сложное взаимодействие путем вставки фрагмента кода посредника. Посредник получает первоначальный сигнал и передает его в действующую реализацию, при этом действуя на сигнал так, чтобы он выполнял то, что ему требуется делать. В данном разделе мы собираемся познакомиться с механизмом перехвата и узнать, каким образом, по своей сути, он является приложением паттерна проектирования Decorator. Если вы не знакомы с паттерном Decorator, то мы разберем его в рамках беседы, и когда мы это сделаем, вы должны будете получить хорошее понимание того, как он работает. Мы начнем с рассмотрения простого примера, который демонстрирует паттерн, и перейдем к обсуждению, как механизм перехвата относится к паттерну Decorator. Пример: реализация аудита В данном примере мы будем реализовывать аудит для ProductRepository . Аудит – это универсальный пример концепции сквозных сущностей: он может потребоваться, но не должен оказывать влияние на основную функциональность чтения и редактирования товаров. Поскольку принцип единичной ответственности предполагает, что мы не должны позволять ProductRepository самому реализовывать аудит, использование паттерна Decorator является наилучшим вариантом. Реализация AuditingProductRepository Реализовать аудит для ProductRepository мы можем путем введения нового класса AuditingProductRepository , который обертывает другой ProductRepository и реализует аудит. Рисунок 9-3 иллюстрирует то, как типы связаны друг с другом. |