Pull to refresh

Comments 38

Property Injection, да ещё и с явной пометкой атрибутами — не очень хорошая идея в общем случае. Лучше использовать Constructor Injection — все зависимости приходят в конструктор, а он их записывает в readonly-поля. Так код и без DI-контейнера использовать проще (для тестов, например), ибо все зависимости видны в одном месте, так и привязки к конкретному DI-контейнеру не будет, т. к. нет нужды использовать атрибуты.

К прочтению настоятельно рекомендуется книжка Dependency Injection in .NET.

Так же следует упомянуть, что свой контейнер можно было и не писать, существующие достаточно расширяемы для получения экземпляров через внешний код, который и мог бы искать зависимости в дереве сцены.
К сожалению, иньекции через конструктор невозможны для объектов, создаваемых Unity, т.е. для всех производных от MonoBehaviour. Можно конечно инжектить в них через методы, но тогда это не read-only поля. И эти поля надо как-то отличать от других полей, явно выделять в коде класса.
А атрибуты на свойствах делают это замечательно. Ну и плюс у вас есть полный контроль над тем, в какие свойства инжектится, а какие — просто свойства
Вы попробуйте на практике, вдруг Вам понравится? ))
Мне кажется наоборот, это мартышкин труд: вручную объявлять параметры конструктора, вручную копировать их в поля класса.
Кроме того, аннотированные атрибутом значения сразу видно (не надо ходить в конструктор, чтобы понять, что зависимость, а что нет).
Насчет мартышкиного труда — это не самая большая проблема, коллега. Например Resharper может генерировать конструкторы за Вас. Заводим поля в классе, а затем жмякаем на кнопочку «Создать конструктор». Решарпер предложит выбрать, какие члены класса мы хотим инициализировать в конструкторе, и генерирует конструктор с соответствующими входными параметрами и присваиваниями.
К сожалению, я не читал книгу, которую рекомендует коллега kekekeks. И могу только предлагать, какие там приведены доводы «за» и «против». Наверное, иньекции через конструктор более безопасны — конструктор можно вызвать только один раз, а в свойства какой-нибудь нехороший код может писать что-угодно и когда угодно.
Однако мне всегда было интересно, что произойдет, если у класса несколько конструкторов? Какой из них контейнер выберет? А как быть, если объект класса создается внешней системой, которая не рассчитана на применение DI-контейнера? Например, если объект класса десериализуется каким-то внешним кодом, как это происходит в случае скриптов в Unity?
А в остальном — полностью с Вами согласен :)
Аргументы «за» конструктор заключаются примерно в том, что во-первых мы весь код получения зависимостей и инициализации сосредотачиваем в одном месте, во-вторых он позволяет гарантировать неизменность данных без дополнительных усилий, в случае со свойствами же необходимо городить что-то типа этого:

IService _service;
public IService Service
{
    set
    {
        if (_service != null)
            throw new InvalidOperationException ();
        _service = value;
    }
    get { return _service; }
}

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

В принципе в случае с Unity можно вместо свойств сделать какой-нибудь init-метод, заполняющий точно так же все поля и проводящий инициализацию и использовать его как конструктор, тогда лишнего кода будет не так уж и много — одна проверка поля _initialized + потеря readonly-модификатора на полях.

Для меня оказалось весьма странным, что Unity не позволяет задать никакой фабрики для создания экземпляров классов, а упорно использует Activator.CreateInstance. Я рекомендую попробовать достучаться до разработчиков, не думаю, что для них это было бы большой проблемой.
+1 к constructor injection. Сразу видны все зависимости, нет привязки к конкретной реализации DI как в случае с атрибутами. Property injection можно оставить только для реализации циклических зависимостей, что чаще всего — bad design.
PS: я фанат Autofac (долгое время пользовался Unity, а затем ninject)
Про ограничения на constructor injection я уже написал выше, в случае Unity3D (не путать с Unity Application Block) это не всегда возможно. А в остальном соглашусь.
А вот вопрос про то, насколько хорошо видны все зависимости при этом. Скажите, а нет ли проблем с тем, чтобы отличить зависимости (которые вы наверное копируете в read-only поля) от других read-only полей? Или таких не бывает, и все что read-only — это стопудово зависимость?
Причем тут поля? Для просмостра зависимостей вы смотрите в сигнатуру конструктора.
Интересно, а как выглядит конструктор сервиса самого верхнего уровня?
Неужели 400 зависимостей передаются в аргументах, чтобы он их пробросил по всему дереву до сервисов самого нижнего уровня?
Если у вас в конструкторе будет больше (пускай) десятка зависимостей — это показывает, что у вас что-то не так с дизайном и нарушение SRP, что опять же заслуга constructor injection. Зачем вашему «сервису верхнего уровня» пробрасывать параметры в нижелижайшие сервисы? они сами подставятся DI. достаточно добавить нижележайшие сервисы в конструктор верхнего уровня.
Если у вас в конструкторе будет больше (пускай) десятка зависимостей — это показывает, что у вас что-то не так с дизайном и нарушение SRP, что опять же заслуга constructor injection

Не только constructor injection, извините )) При properties injection это тоже очень хорошо заметно
Да, я похоже не понял сразу, как оно будет выглядеть. Попробовал смоделировать — всё нормально
Скрытый текст
    public interface IDAO1
    {
    }

    public interface IDAO2
    {
    }

    public interface IService1
    {
    }

    public interface IService2
    {
    }

    public class DAO1 : IDAO1
    {
    }

    public class DAO2 : IDAO2
    {
    }

    class Service1 : IService1
    {
        readonly IDAO1 dao1;
        readonly IDAO2 dao2;

        public Service1(IDAO1 dao1, IDAO2 dao2)
        {
            this.dao1 = dao1;
            this.dao2 = dao2;
        }
    }

    class Service2 : IService2
    {
        readonly IService1 service1;

        public Service2(IService1 service1)
        {
            this.service1 = service1;
        }

        public void Run()
        {
        }
    }

    class ExtrnalCode
    {
        public void Run()
        {
            var dao1 = new DAO1();
            var dao2 = new DAO2();
            var service1 = new Service1(dao1, dao2);
            var service2 = new Service2(service1);
            service2.Run();
        }
    }
Очень интересно. Если автору есть еще чем поделиться по подобной теме, пожалуйста пишите.
Какие паттерны и с каким успехом Вы еще использовали?
Спасибо за доброе слово! Конечно можно написать про best practices, которые из всего этого вытекают, но похоже, темой заинтересовались не многие.
Я подумаю, чем еще нетривиальным могу поделиться с хабрсообществом. Есть мысли прикрутить ко всему этому AOP ;)
Увы, AOP при невозможности самостоятельно создавать инстансы нормально будет работать только в виде патчей сборки, как это делает PostSharp, например.
Тема очень интересна, хотя я и не связан с Unity, но best practicies всегда интересны вне зависимости от сферы применения.
Будьте любезны пояснить мысль. Что имеется в виду? Что DI это плохо? Или что-то иное?
И кому принадлежит цитата?
Контейнер не обязательно сам по себе глобален. Строго говоря у вас может быть несколько по-разному сконфигурированных экземпляров контейнера в одном AppDomain. DI-контейнер — всего лишь клей, помогающий выстроить граф объектов приложения. Вот ServiceLocator — он да, существенно ближе к глобальным переменным.
Соглашусь с коллегой kekekeks. DI-контейнер — это всего лишь переменная в вашей программе. Делать ли ее глобальной и доступной отовсюду — ваше личное дело. Вот например:

public void Start()
{
	var container = CreateContainer();
	_gameMain = container.Resolve<GameMain>();
}


Здесь контейнер не является глобальной переменной и создается только лишь для того, чтобы создать и нужным образом сконфигурировать объект _gameMain.
А каким образом следует инжектить зависимости в объекты, создаваемые Unity? В моем случае все игровые объекты и компоненты создаются на лету. Пока только придумал использовать фабрику и передавать ей ссылку на Container (что, как я понимаю, не есть хорошо, да и выглядит хреново):
public GameObject CreateMyObject() {
    GameObject go = new GameObject();
    MyComp comp = go.AddComponent<MyComp>();
    container.BuildUp(comp);
    return go;
}
К сожалению да, только через BuildUp. Соответственно если это объекты в дереве сцены, то находим их и инжектим в них зависимости на старте локации. Если это динамически создаваемые объекты, например инстанцированные префабы, то нужна фабрика. Фабрика может иметь зависимость от самого контейнера. А для это нужно зарегать контейнер в нем самом ))
Спасибо! Обязательно почитаю
и как? Сейчас вот выбираю, какой DIC взять, и первый в гугле выдаётся именно этот «странный» IoC.
Вы пробовали его использовать, или так и продолжаете использовать свой? Свой контейнер тестировали на производительность?
StrangeIOC использовать можно, но аккуратнее с командами. При большом количестве команд получаем просадку по производительности. У нас используется на одном проекте, там стараемся обходиться обычными эвентами C#.
Свой контейнер не профайлили — как то не понадобилось. На проектах, в которых он используется проблем с производительностью нет.
Если я правильно понял ваш ответ, то вы попробовали StrangeIoC на более крупном проекте, где имели кое-какие проблемы с количеством команд. Но на большинстве своих проектов продолжаете использовать свой UnityDI, и вас он более чем устраивает.
Я правильно вас понял?
Да, абсолютно правильно.
спасибо.
Попробую и ваш контейнер, и StrangeIoC, может быть, ещё и Lightweight-Ioc-Container. Отпишусь сюда по результатам использования.
Тоже пытаюсь юнити приобщить к светлому, но классически DI контейнер чужероден. В юньке все связи делаются драдропом, объекты могут менять свое положение дизами, и каждый раз править пути это ужас. Стандартное решение в юнити есть, вместо интерфейсов юзать абстрактные классы, тогда разрешать зависимости можно дропом. Но тут другая проблема, абстрактный класс может быть только один в цепочке наследования, поэтому если сервис реализует несколько ролей то, такой вид инекции не сработает.
Не уверен, что понял правильно. Мы используем интерфейсы вместо абстрактных классов — их можно реализовывать сколько угодно. Про то что скрестить DI с Unity непросто — согласен. Я постепенно переосмысливаю свой подход и постараюсь в скором времени выкатить более адаптированную версию
А на каких платформах работает ваш контейнер? Ios? Android? Win/Mac/Linux?
Это чистый C# и в теории работает на любых платформах. Проверяли на PC, Mac, iOS и Android
На iOs не работает кодогенерация, т.к. код компилируется, а не выполняется на лету, поэтому все что использует кодогенерацию не будет работать на iOs. Многие контейнеры используют кодогенерацию, например Ninject, поэтому они не будут работать на iOs. Глянул ваш код, вроде кодогенерации нет, значит должно работать везде.
Некропостинг, но все же.
А вот представим такой вариант:
  • Ввод обрабатывается где-то в другом месте, все, что мы можем — подписаться на события движения влево/вправо, а уж кто их там вызвал — не важно
  • В IControlledCharacter выполняется подписка (видимо это дело станет абстрактным классом) абстрактных методов MoveLeft/MoveRight на эти события
  • Наследники IControlledCharacter тихо висят на своих местах и лихо крутят кораблями вне зависимости от контроллера, даже не подозревая о его существовании
  • Профит?

Мне кажется, такой подход несколько более прозрачен, чем создание контейнеров. По крайней мере, в данном примере.
Sign up to leave a comment.

Articles