Антипаттерн IOC Container — Marcus Technical Services

Прежде чем я получу сопровождение фонаря в стиле Франкенштейна к виселице, позвольте мне заверить вас: я любовь внедрение зависимости (основа инверсии управления). Это одна из основных заповедей разработки кода SOLID: _ следует «полагаться на абстракции, а не на конкреции _.” Я также поддерживаю настоящую инверсию управления — изменение _ поток _ приложения с помощью более сложной формы внедрения зависимостей. Многие современные фреймворки заимствуют термин «IOC», чтобы убедить нас в своей полезности, потому что, в конце концов, почему бы еще их так называть? Потому что они продают _ шипеть _: способ для программистов делать что-то, не разбираясь в этом и не заморачиваясь над его дизайном. Так развивался IOC Container. Он часто появляется как игрушка-взломщик внутри MVVM Framework (https://marcusts.com/2018/04/06/the-mvvm-framework-anti-pattern/). Он страдает той же близорукостью.

«Вода, вода повсюду… и ни капли для питья».

Не очень счастливый моряк на спасательном плоту

Это вовсе не контейнеры «IOC»

Чтобы квалифицироваться как форма инверсии _ Контроль _ эти так называемые «контейнеры ввода-вывода» должны будут контролировать что-то вроде потока приложений. Все, что делают контейнеры, — это хранят глобальные переменные. В более продвинутых контейнерах программист может вставить логику конструктора, чтобы определить, как создать сохраняемую переменную. То есть _ нет _ контроль. Это _ создание экземпляра _ или же _ назначение _. Эти сущности следует называть контейнерами DI («Внедрение зависимостей»). Если мы хотим, чтобы нас воспринимали всерьез за наши идеи, мы должны быть осторожны, чтобы не преувеличивать их особенности.

Глобальные переменные — плохой дизайн

Так называемый «контейнер IOC» представляет собой словарь _ глобальные переменные _ который обычно доступен из _ в любом месте _ внутри программы. Это по сути нарушает принципы кодирования C#. C# и SOLID требуют, чтобы переменные класса были _ как можно более приватно _. Это сохраняет слабосвязанную программу, поскольку взаимодействие между классами должно управляться интерфейсными контрактами. Представьте, если бы вы передали этот код своему техническому руководителю:

public interface IMainViewModel { } public class MainViewModel : IMainViewModel { } public partial class App : Application { public App() { GlobalVariables.Add(typeof(IMainViewModel), () => new MainViewModel()); InitializeComponent(); MainPage = new MainPage() { BindingContext = GlobalVariables[typeof(IMainViewModel)] }; } public static Dictionary<Type, Func<object>> GlobalVariables = new Dictionary<Type, Func<object>>(); }

Ты будешь _ уволенный _. «Что Вы думаете о?» — требует ваш начальник. «Почему бы просто не создать MainViewModel там, где это необходимо, и держать это в тайне?» Затем вы предоставляете этот код:

public interface IMainViewModel { } public class MainViewModel : IMainViewModel { } public static class AppContainer { public static IContainer Container { get; set; } } public partial class App : Application { public App() { var containerBuilder = new ContainerBuilder(); containerBuilder.RegisterType<MainViewModel>().As<IMainViewModel>(); AppContainer.Container = containerBuilder.Build(); InitializeComponent(); MainPage = new MainPage() { BindingContext = AppContainer.Container.Resolve<IMainViewModel>()}; } public static IContainer IOCContainer { get; set; } }

Теперь вас похлопывают по спине. Великолепно! Кроме маленькой проблемы: _ это тот же код _. Оба решения полагаются на _ глобальный статический словарь _ переменных. Мы не глобализируем какую-либо переменную класса в программе, если только эта переменная не должна быть легко доступна из любого места. Это может относиться к определенным услугам, но почти ни к чему другому. Действительно, предшественником современных контейнеров IOC является «локатор сервисов». Вот где это должно было закончиться. Давайте рефакторим и расширим последний пример, чтобы добавить вторую модель представления, которую мы небрежно вставляем в контейнер IOC:

public static class AppContainer { static AppContainer() { var containerBuilder = new ContainerBuilder(); containerBuilder.RegisterType<MainViewModel>().As<IMainViewModel>(); containerBuilder.RegisterType<SecondViewModel>().As<ISecondViewModel>(); Container = containerBuilder.Build(); } public static IContainer Container { get; set; } }

Внутри конструктора второй страницы мы делаем ошибку. Мы запрашиваем неправильную модель представления:

public partial class SecondPage : ContentPage { public SecondPage() { BindingContext = AppContainer.Container.Resolve<IMainViewModel>(); InitializeComponent(); } }

Ой! Почему нам это позволено? Поскольку все модели представления globalпоэтому к ним можно получить доступ — правильно или неправильно — из _ где угодно, любым потребителем, по любой причине _. Это классический анти-шаблон: то, что вы обычно не должны делать.

Контейнеры IOC не являются безопасными во время компиляции

Это на самом деле компилируется, хотя SecondViewModel *не* реализует IMainViewModel.

containerBuilder.RegisterType<MainViewModel>().As<IMainViewModel>(); containerBuilder.RegisterType<SecondViewModel>().As<IMainViewModel>();

Во время работы вылетает! Целью и обязанностью всех платформ C# является создание _ безопасный тип во время компиляции _ взаимодействия. Время выполнения крайне ненадежно по сравнению с ним.

Контейнеры IOC создают новые экземпляры переменных по умолчанию

Быстрый тест: верен ли этот тест на равенство?

var firstAccessedMainViewModel = AppContainer.Container.Resolve<IMainViewModel>(); var secondAccessedMainViewModel = AppContainer.Container.Resolve<IMainViewModel>(); var areEqual = ReferenceEquals(firstAccessedMainViewModel, secondAccessedMainViewModel);

Ответ _ нет _, это _ ЛОЖЬ _. Контейнер обычно выдает отдельный экземпляр для каждой запрошенной переменной. Это шокирует, поскольку большинство переменных должны сохранять свое состояние во время выполнения. Представьте себе создание модели представления системных настроек:

containerBuilder.RegisterType<SettingsViewModel>().As<ISettingsViewModel>();

Вам нужна эта модель представления в двух местах: в профиле (где хранятся настройки) и главная страница и ее модель просмотра (где они потребляются).

var settingsViewModel = AppContainer.Container.Resolve<ISettingsViewModel>();

Таким образом, мы создаем ту же самую дилемму, которую только что упоминали:

В настройках:
var settingsViewModel = AppContainer.Container.Resolve<ISettingsViewModel>();
На главной:
var settingsViewModel = AppContainer.Container.Resolve<ISettingsViewModel>();

Пользователь открывает меню, переходит в свой профиль и меняет одну из настроек. Они закрывают это окно, закрывают меню и смотрят на свой главный экран. Изменения видны? Нет! Он хранится в другой переменной. Переменная основных настроек теперь «устарела», поэтому на главном экране отображаются неверные значения. Для этого есть официальный взлом. Вместо:

containerBuilder.RegisterType<MainViewModel>().As<IMainViewModel>(); containerBuilder.RegisterType<SecondViewModel>().As<ISecondViewModel>();

Мы пишем:

containerBuilder.RegisterType<MainViewModel>().As<IMainViewModel>().SingleInstance(); containerBuilder.RegisterType<SecondViewModel>().As<ISecondViewModel>().SingleInstance();

SingleInstance расширение гарантирует, что всегда будет возвращаться один и тот же экземпляр переменной. Исключением из этого руководства является список списков:

public interface IChildViewModel { } public class ChildViewModel : IChildViewModel { } public interface IParentViewModel { IList<IChildViewModel> Children { get; set; } } public class ParentViewModel : IParentViewModel { public IList<IChildViewModel> Children { get; set; } }

Единственный способ для ParentViewModel добавить список дочерних элементов — это установить их модели просмотра _ однозначно _. Итак, в этом случае регистрация будет:

containerBuilder.RegisterType<ChildViewModel>().As<IChildViewModel>();

Мы делаем _ нет _ включить SingleInstance() суффикс.

Контейнеры IOC создают экземпляры классов без гибкости или понимания

Программисты учатся _ отделить _ классы для уменьшения взаимозависимости («разветвление») с другими классами. Но это не значит, что мы стремимся отказаться от контроля. Класс может быть создан и может быть уничтожен. Создание экземпляра важно, потому что именно здесь происходят все формы внедрения зависимостей. Контейнер IOC крадет этот контроль у нас. Контейнер IOC анализирует конструктор каждого класса хранилища, чтобы определить, как создать экземпляр. Он ищет путь наименьшего сопротивления к построению класса. Но это не гарантирует разумного или предсказуемого решения. Например, эти два класса используют один и тот же интерфейс, но устанавливают для логического значения интерфейса разные значения:

public interface ICanBeActive { bool IsActive { get; set; } } public interface IGeneralInjectable : ICanBeActive { } public class FirstPossibleInjectedClass : IGeneralInjectable { public FirstPossibleInjectedClass() { IsActive = true; } public bool IsActive { get; set; } } public class SecondPossibleInjectedClass : IGeneralInjectable { public SecondPossibleInjectedClass() { IsActive = false; } public bool IsActive { get; set; } }

Чтобы классы учитывались для внедрения, мы должны добавить их «как» IGeneralInjectable:

containerBuilder.RegisterType<FirstPossibleInjectedClass>().As<IGeneralInjectable>(); containerBuilder.RegisterType<SecondPossibleInjectedClass>().As<IGeneralInjectable>();

Обратите внимание, что в остальном классы идентичны, и что их конструкторы также точно совпадают. Вот класс, который получает инъекцию только _ один _ из этих двух классов:

public class ClassWithConstructors { public bool derivedIsActive { get; set; } public ClassWithConstructors(IGeneralInjectable injectedClass) { derivedIsActive = injectedClass.IsActive; } } containerBuilder.RegisterType<ClassWithConstructors>();

Теперь мы запрашиваем у контейнера IOC экземпляр ClassWithConstructors:

var whoKnowsWhatThisIs = AppContainer.Container.Resolve<ClassWithConstructors>();

Итак, как контейнер IOC решит, что вводить для создания ClassWithConstructors? Когда контейнер проверяет кандидатов на IGeneralInjectableон найдет двух кандидатов:

  1. FirstPossibleInjectedClass
  2. SecondPossibleInjectedClass

Оба класса не имеют параметров, что делает их равными. Контейнер IOC выберет первый найденный. Что бы это ни было, оно будет _ неправильный _. Это потому, что два класса принимают разные решения для IsActive. Поскольку оба являются законными, и только один из них может быть разрешен, контейнеру IOC нельзя доверять это решение. Это должно произвести _ ошибка компилятора _. Но логика «черного ящика» внутри контейнера IOC маскирует это и выдает результат, на который мы не можем полагаться. Вас может удивить, какой именно класс «победил» в этом состязании. Это было _ добавлен последний класс _ в контейнер! Я проверил это, изменив порядок, в котором они были добавлены, и, конечно же, последовала инъекция. Есть и другие проблемы. Интерфейсы — это гибкие контракты, и класс может реализовать любое их количество. Для каждого нового реализованного интерфейса класс *должен* объявить его, используя соглашение as, иначе он не будет работать:

public interface IOtherwiseInjectable { } public interface IGeneralInjectable : ICanBeActive { } public class FirstPossibleInjectedClass : IGeneralInjectable, IOtherwiseInjectable { public FirstPossibleInjectedClass() { IsActive = true; } public bool IsActive { get; set; } } containerBuilder.RegisterType<FirstPossibleInjectedClass>().As<IGeneralInjectable>(); containerBuilder.RegisterType<FirstPossibleInjectedClass>().As<IOtherwiseInjectable>();

Это можно сделать вручную, конечно. А если их десятки? Что делать, если вы пропустите один? Контейнер может быть неправильно введен без каких-либо предупреждений или симптомов.

Выводы

Контейнеры IOC являются анти-шаблоном, потому что:

  1. Они вовсе не МОК; это игрушки для инъекций зависимостей;
  2. Они создают глобальные переменные, когда они не нужны;
  3. Они «слишком доступны» — в приложении класса C# конфиденциальность правит днем. Мы не хотим, чтобы все имели доступ ко всему остальному.
  4. Они выдают новые экземпляры переменных, которые почти всегда должны быть одиночками;
  5. Они используют сверхупрощенную логику при создании экземпляров классов, которая не поддерживает уровень сложности и нюансов, присутствующих в большинстве приложений C#.

Жесткие доказательства

я создал Xamarin.Forms мобильное приложение для демонстрации исходного кода в этой статье. Исходный код доступен на GitHub по адресу https://github.com/marcusts/xamarin-forms-annoyances. Решение называется <strong>IOCAntipattern.sln</strong>.

Я также спроектировал намного умнее — и крошечный! — Контейнер DI, доступен бесплатно на https://github.com/marcusts/Com.MarcusTS.SmartDI.

Весь код публикуется с открытым исходным кодом и без обременений.

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *