Антипаттерн 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
он найдет двух кандидатов:
FirstPossibleInjectedClass
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 являются анти-шаблоном, потому что:
- Они вовсе не МОК; это игрушки для инъекций зависимостей;
- Они создают глобальные переменные, когда они не нужны;
- Они «слишком доступны» — в приложении класса C# конфиденциальность правит днем. Мы не хотим, чтобы все имели доступ ко всему остальному.
- Они выдают новые экземпляры переменных, которые почти всегда должны быть одиночками;
- Они используют сверхупрощенную логику при создании экземпляров классов, которая не поддерживает уровень сложности и нюансов, присутствующих в большинстве приложений C#.
Жесткие доказательства
я создал Xamarin.Forms
мобильное приложение для демонстрации исходного кода в этой статье. Исходный код доступен на GitHub по адресу https://github.com/marcusts/xamarin-forms-annoyances. Решение называется <strong>IOCAntipattern.sln</strong>
.
Я также спроектировал намного умнее — и крошечный! — Контейнер DI, доступен бесплатно на https://github.com/marcusts/Com.MarcusTS.SmartDI.
Весь код публикуется с открытым исходным кодом и без обременений.