Можно сделать очень мало

В предыдущей статье Простые интерфейсы C#: сервис я рассказывал об интерфейсах сервисов, которые можно упростить и расширить за счет использования методов расширения. Аналогичным образом в этой статье будет обсуждаться еще один простой стиль интерфейса, который благодаря своей простоте обеспечивает масштабируемость: контейнерные интерфейсы.

Контейнерный интерфейс

Интерфейс контейнера является держателем данных. Эти «данные» затем используются некоторым сервисом или процессом. Во многих отношениях интерфейсы контейнеров представляют собой простое приближение к шаблону компоновщика, без огромного беспорядка сложной структуры методов.

Повторюсь из моей последней статьи,

Поскольку интерфейс — это контракт, сохранение его как можно меньшего размера дает наибольшую выгоду. Это особенно верно, если интерфейс содержит все необходимые функции, которые могут вам понадобиться.

По сути, мы хотим минимизировать площадь поверхности интерфейса. Это значительно упростит расширение его возможностей, сделав реализацию новых реализаций тривиальной (мы надеемся). Давайте рассмотрим пример.

Интерфейс IServiceCollection

Если вы знакомы с .NET Core, вы, вероятно, сталкивались с интерфейсом IServiceCollection. Это основное направление внедрения зависимостей в модели контейнера IoC от Microsoft. Давайте посмотрим на определение интерфейса.

Итак, позвольте мне провести вас по этому коду построчно.

Строка 11 => Это список ServiceDescriptor

Да. Вот и все. Несмотря на всю функциональность, которую этот интерфейс предоставляет основной функции .NET, это просто список ServiceDescriptor . То, что интерфейс наследуется, скорее всего, вопрос удобства.

Примечание. Конечно, чтобы это работало, необходимо создать базовую службу, которая будет принимать этот набор значений. Но это не отменяет простоты использования с точки зрения разработчика.

Почему это работает? Ну, это работает, потому что класс ServiceDescriptor — это модель, которая просто содержит описания времени жизни, контракта и реализации в объекте в стиле DTO. Вот уменьшенная версия класса ServiceDescriptor.

Для этого класса требуется только свойство Lifetime и свойство ServiceType. После этого все остальное необязательно. Чтобы гарантировать, что состояние объекта всегда поддерживается, экземпляры этого класса являются неизменяемыми, и используются очень специфические конструкторы для обеспечения различных допустимых состояний. Это очень важно для такого типа приложений, так как наличие ImplementationType и ImplementationInstance не имеет никакого смысла.

Теперь возникает вопрос, как это делает нашу жизнь проще? С точки зрения разработчика, я не думаю, что это само по себе. Вместо этого мы действительно должны ввести немного синтаксического сахара, чтобы облегчить нашу боль. Давайте использовать методы расширения!

Начнем с создания набора расширений для базовых жизненных циклов контейнеров внедрения зависимостей, поддерживаемых перечислением ServiceLifetime.

Я реализовал их без соответствующих нулевых проверок, но в остальном реализации одинаковы. Я также пропустил некоторые перегрузки. Вы всегда можете добавить их по своему усмотрению (или вы знаете… использовать реальную реализацию).

Теперь, если вы выполнили любой уровень внедрения зависимостей .NET Core, вы заметите, что это только верхушка айсберга. Следующий пример кода из среды выполнения предназначен для настройки параметров.

Как видите, мы можем продолжать расширять этот простой интерфейс для создания сложных инфраструктур внедрения зависимостей, просто добавляя класс модели в список. Прелесть этой методики в ее простоте. Еще лучше то, что если мы решим реализовать нашу собственную версию IServiceCollection или IServiceProvider (созданный контейнер IoC), мы все равно сможем повторно использовать весь код нашего приложения. Хорошим примером этого является система DI Lamar, где вместо ServiceCollection они называют свой контейнер ServiceRegistry.

Конечно, использование другого контейнера обычно означает, что мы хотим использовать некоторые дополнительные функции или синтаксический сахар, поэтому код, скорее всего, изменится. Но мы не теряем предоставленные расширения.

Еще один короткий пример

Мы все уже видели IServiceCollection в .NET Core. Но что, если мы хотим что-то сделать в нашей собственной системе; может что-то немного другое.

Давайте воспользуемся аналогичной тактикой, но применим ее для системы рабочих процессов ASP.NET Core.

Рабочий процесс

Первым шагом является то, что мы хотим выполнить некоторые действия в рабочем процессе, поэтому мы определим интерфейсы выше. У нас есть интерфейс IPage для определения результата в конце запуска рабочего процесса. IActivity может изменить объект контекста IActivityContext, что позволит нам изменить результат по мере необходимости.

Наконец, у нас есть набор дескрипторов активности IActivityCollection, похожий на IServiceCollection, где может быть предоставлен Type или экземпляр IActivity.

Дескрипторы страницы

Далее нам нужно определить сами страницы с помощью класса PageDescriptor. Для этого класса требуется коллекция действий PageType и OnNext. При желании вы можете включить действие отмены. В этом случае мы определим на основе текущего состояния рабочего процесса, что происходит, когда пользователь активирует «следующую» команду (т. е. «ОК», «Сохранить», «Экспорт») или команду «отменить» (т. е. « Отмена», «Назад»). Это должно быть определено в действиях контроллера, которые активируют рабочий процесс. Но это означает, что самим страницам напрямую не нужно знать следующую стадию, так как это определяется во время выполнения.

Будем считать, что первая страница, добавленная в коллекцию, является начальной страницей. Тогда все страницы после этого являются возможными целями.

Создание страниц

Начнем с определения некоторых расширений для создания действий.

Далее давайте создадим базовый набор расширений для определений страниц.

Вы можете заметить класс, который я еще не определил, под названием ActivityCollection. Предположим, что это внутренняя реализация интерфейса IActivityCollection. Однако реализация несколько не имеет значения для обсуждения здесь.

Попробуйте!

Теперь, когда у нас есть несколько расширений, давайте добавим несколько «нереализованных» расширений, чтобы показать пример. Мы также включим несколько «страниц», которые у нас могут быть. Отсюда вам придется использовать свое воображение!

И, наконец, давайте покажем пример использования наших новых расширений в фабричном методе.

Здесь вы видите, что мы можем создать довольно сложный рабочий процесс из простого набора интерфейсов. В частности, большая часть работы выполняется двумя интерфейсами контейнера. В этом сила правильного дизайна интерфейса и его простота.

Заключение и подпись

В этой статье я показал реальный пример, созданный командой .NET Core Team, и пример рабочего процесса. Оба используют одинаковую технику при построении системы, интерфейс контейнера.

Я признаю, однако, что эта техника может ограничить ваши проекты. Мы можем в конечном итоге проиграть в отношении синтаксического сахара. Нам нужно обойти нашу простоту, если мы хотим создать «идеальный» синтаксис для нашей системы. Это добавит сложности в реализациях и расширениях. Однако при соответствующей инкапсуляции разработчик-потребитель не заметит разницы.

Если у вас есть какие-либо мысли или предложения, пожалуйста, прокомментируйте ниже. И обязательно подпишитесь на меня на Medium, так как я постоянно публикую новые темы!

До скорого!