Зависимость Инъекция и производительность разработки

Абстрактный

За последние несколько месяцев я программировал легкий игровой движок на базе С# с абстракцией API и системой сущностей/компонентов/скриптов. Вся идея заключается в том, чтобы облегчить процесс разработки игры в XNA, SlimDX и т.д., Создав архитектуру, подобную архитектуре Unity.

Проблемы проектирования

Как известно большинству разработчиков игр, существует множество разных сервисов, необходимых для доступа к вашему коду. Многие разработчики прибегают к использованию глобальных статических экземпляров, например. менеджер Render (или композитор), Scene, Graphicsdevice (DX), Logger, Input, Viewport, Window и т.д. Существуют альтернативные подходы к глобальным статическим экземплярам/синглонам. Один из них - дать каждому классу экземпляр классов, к которым он должен получить доступ, либо через инсталляцию зависимостей конструктора, либо конструктор/свойство (DI), другой - использовать глобальный локатор службы, например StructureMap ObjectFactory, где локатор службы обычно настраивается как контейнер IoC.

Инъекция зависимостей

Я выбрал путь DI по многим причинам. Наиболее очевидным из которых является тестируемость, программирование против интерфейсов и наличие всех зависимостей каждого класса, предоставляемого им через конструктор, эти классы легко тестируются, поскольку тестовый контейнер может создавать требуемые сервисы или издеваться над ними и загружать их в каждый испытуемый класс. Другая причина для выполнения DI/IoC была, верьте или нет, для повышения удобочитаемости кода. Нет более огромного процесса инициализации создания экземпляров всех различных сервисов и ручного создания классов со ссылками на требуемые сервисы. Конфигурирование ядра (NInject)/Registry (StructureMap) удобно дает единую точку конфигурации для движка/игры, в которой собираются и настраиваются сервисные реализации.

Мои проблемы

  • Мне часто кажется, что я создаю интерфейсы для интерфейсов.
  • Моя производительность резко снизилась, так как все, что я делаю, это беспокоиться о том, как делать вещи DI-way, а не быстро и просто глобально статически.
  • В некоторых случаях, например, при создании экземпляров новых сущностей во время выполнения требуется создать доступ к контейнеру/ядру IoC для создания экземпляра. Это создает зависимость от самого контейнера IoC (ObjectFactory в SM, экземпляр ядра в Ninject), что действительно противоречит причине его использования в первую очередь. Как это можно решить? Приводятся абстрактные заводы, но это еще больше усложняет код.
  • В зависимости от требований к сервису конструкторы некоторых классов могут стать очень большими, что сделает класс совершенно бесполезным в других контекстах, где и если IoC не используется.

В основном выполнение DI/IoC резко замедляет мою производительность, а в некоторых случаях еще больше усложняет код и архитектуру. Поэтому я не уверен, что это путь, за которым я должен следовать, или просто отказаться и сделать то, что старомодно. Я не ищу ни одного ответа, говоря, что я должен или не должен делать, кроме обсуждения вопроса о том, стоит ли использовать DI в долгосрочной перспективе, а не использовать глобальный статический/однопользовательский способ, возможные плюсы и минусы, которые я забыл и возможные решения моих проблем, перечисленных выше, при работе с DI.

Ответ 1

Должны ли вы вернуться к старому? Мой короткий ответ - нет. DI имеет множество преимуществ по всем причинам, о которых вы упомянули.

Мне часто кажется, что я создаю интерфейсы для интерфейса.

Если вы это делаете, вы можете нарушить Принцип повторных абстракций (RAP)

В зависимости от требований к сервису, некоторые конструкторы классов могут очень большой, что сделает класс совершенно бесполезным в других контексты, где и если IoC не используется.

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

Чтобы сделать DI, вам не нужно иметь интерфейс, DI - это то, как вы получаете свои зависимости в свой объект. Создание интерфейсов может быть необходимым способом, чтобы заменить зависимость для целей тестирования. Если объектом зависимостей не является:

  • Легко изолировать
  • Не разговаривает с внешними подсистемами (файловая система и т.д.)

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

В некоторых случаях, например, при создании новых сущностей во время выполнения, один для создания экземпляра требуется доступ к контейнеру/ядру IoC. Это создает зависимость от самого контейнера IoC (ObjectFactory в SM, экземпляр ядра в Ninject), который действительно идет против причины его использования в первую очередь. Как это может быть решены? Приходят на ум абстрактные заводы, но это еще больше усложняет код.

Что касается зависимости от контейнера IOC, вы никогда не должны зависеть от него в своих клиентских классах. И они не должны.

Чтобы правильно использовать инъекцию зависимостей, необходимо понять концепцию Root of Composition. Это единственное место, где нужно ссылаться на ваш контейнер. На этом этапе строится весь ваш граф объектов. Как только вы поймете это, вы поймете, что вам не нужен контейнер в ваших клиентах. Поскольку каждый клиент просто вводит свою зависимость.

Есть также МНОГО других творческих моделей, которые вы можете использовать, чтобы упростить конструкцию: Предположим, вы хотите создать объект со многими зависимостями, подобный этому:

new SomeBusinessObject(
    new SomethingChangedNotificationService(new EmailErrorHandler()),
    new EmailErrorHandler(),
    new MyDao(new EmailErrorHandler()));

Вы можете создать конкретный factory, который знает, как это сделать:

public static class SomeBusinessObjectFactory
{
    public static SomeBusinessObject Create()
    {
        return new SomeBusinessObject(
            new SomethingChangedNotificationService(new EmailErrorHandler()),
            new EmailErrorHandler(),
            new MyDao(new EmailErrorHandler()));
    }
}

И затем используйте его следующим образом:

 SomeBusinessObject bo = SomeBusinessObjectFactory.Create();

Вы также можете использовать бедный mans di и создать конструктор, который не принимает никаких аргументов:

public SomeBusinessObject()
{
    var errorHandler = new EmailErrorHandler();
    var dao = new MyDao(errorHandler);
    var notificationService = new SomethingChangedNotificationService(errorHandler);
    Initialize(notificationService, errorHandler, dao);
}

protected void Initialize(
    INotificationService notifcationService,
    IErrorHandler errorHandler,
    MyDao dao)
{
    this._NotificationService = notifcationService;
    this._ErrorHandler = errorHandler;
    this._Dao = dao;
}

Тогда просто кажется, что он работал:

SomeBusinessObject bo = new SomeBusinessObject();

Использование Poor Man DI считается неудачным, когда ваши реализации по умолчанию находятся во внешних сторонних библиотеках, но менее плохи, когда у вас хорошая реализация по умолчанию.

Тогда, очевидно, есть все контейнеры DI, построители объектов и другие шаблоны.

Итак, вам нужно только подумать о хорошем шаблоне творения для вашего объекта. Сам по себе объект не заботится о том, как создавать зависимости, на самом деле он делает их БОЛЕЕ сложными и заставляет их смешивать 2 типа логики. Поэтому я не верю, что использование DI должно привести к потере производительности.

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

public interface IDataAccessFactory
{
    TDao Create<TDao>();
}

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

public class ConcreteDataAccessFactory : IDataAccessFactory
{
    private readonly IocContainer _Container;

    public ConcreteDataAccessFactory(IocContainer container)
    {
        this._Container = container;
    }

    public TDao Create<TDao>()
    {
        return (TDao)Activator.CreateInstance(typeof(TDao),
            this._Container.Resolve<Dependency1>(), 
            this._Container.Resolve<Dependency2>())
    }
}

Заметьте, что я использовал активатор, хотя у меня был контейнер Ioc, важно отметить, что factory должен построить новый экземпляр объекта, а не просто предположить, что контейнер предоставит новый экземпляр, поскольку объект может быть зарегистрирован с разными временами жизни (Singleton, ThreadLocal и т.д.). Однако в зависимости от того, какой контейнер вы используете, некоторые могут сгенерировать эти заводы для вас. Однако, если вы уверены, что объект зарегистрирован в течение переходного периода, вы можете просто решить его.

EDIT: добавление класса с абстрактной зависимостью factory:

public class SomeOtherBusinessObject
{
    private IDataAccessFactory _DataAccessFactory;

    public SomeOtherBusinessObject(
        IDataAccessFactory dataAccessFactory,
        INotificationService notifcationService,
        IErrorHandler errorHandler)
    {
        this._DataAccessFactory = dataAccessFactory;
    }

    public void DoSomething()
    {
        for (int i = 0; i < 10; i++)
        {
            using (var dao = this._DataAccessFactory.Create<MyDao>())
            {
                // work with dao
                // Console.WriteLine(
                //     "Working with dao: " + dao.GetHashCode().ToString());
            }
        }
    }
}

В основном выполнение DI/IoC резко замедляет мою производительность и некоторые случаи еще более усложняют код и архитектуру

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

Loose Coupling и Big Picture

EDIT: Наконец, я хотел бы указать, что не каждый объект и зависимость нуждаются или должны быть введены в зависимость, сначала подумайте, действительно ли то, что вы используете, считается зависимостью:

Каковы зависимости?

  • Конфигурация приложения
  • Ресурсы системы (часы)
  • Сторонние библиотеки
  • База данных
  • WCF/Сетевые службы
  • Внешние системы (файл/электронная почта)

Любой из вышеуказанных объектов или соавторов может оказаться вне вашего контроля и вызывать побочные эффекты и разницу в поведении и затруднять тестирование. Это время, чтобы рассмотреть абстракцию (класс/интерфейс) и использовать DI.

Каковы не зависимости, на самом деле не требуется DI?

  • List<T>
  • MemoryStream
  • Строки/Примитивы
  • Листовые объекты /Dto 's

Объекты, такие как приведенные выше, могут быть просто созданы при необходимости с использованием ключевого слова new. Я бы не предложил использовать DI для таких простых объектов, если нет особых причин. Рассмотрим вопрос, находится ли объект под вашим полным контролем и не вызывает каких-либо дополнительных графиков объектов или побочных эффектов в поведении (по крайней мере, все, что вы хотите изменить/контролировать поведение или тест). В этом случае просто обновите их.

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