Pull to refresh

Comments 14

Вообще-то, обычно в Unity регистрируют не экземпляры, а типы или фабрики — как раз чтобы тяжелые сервисы инициализировались по месту.

Ваш подход плох тем, что (а) вы сразу оперируете экземплярами и (б) вы выносите часть логики наружу компонента. А вдруг инициализация компонента перестала быть потокобезопасной? А вдруг операции инициализации, на самом деле, не приносят выигрыша при параллельном запуске? Или у вас появилась дополнительная зависимость, усложняющая граф?
Как я упоминал в самом начале, для моих задач (бекенд за балансировщиком) мне выгоднее провести инициализацию всего тяжелого именно на старте. Я не хочу увеличивать время отклика первых пришедших пользователей. Поэтому стараюсь на старте загрузить по-максимуму, а отложенную инициализацию использую очень осторожно.
Это также помогает диагностике. Потому что приложение в случае проблем упадет прямо здесь и сейчас в момент запуска, когда администратор занят непосредственно запуском. А не через несколько часов, когда кто-то из клиентов вызовет крайне редкоиспользуемый метод.

Наверное, следовало все-таки вынести в статью, почему я предпочитаю явную инициализацию экземпляров вместо регистрации типов.
Главная причина — это понятность и читаемость кода. Когда открываешь сравнительно простой чужой проект с точки зрения бизнес-логики и видишь в инициализации IoC-контейнера адскую мешанину из многоэтажных конструкций RegisterType, Resolve и InjectionConstructor, то… тоска и желание закрыть это и больше никогда не открывать. Очень тяжело разобраться сходу какая реализация интерфейса будет использоваться в качестве зависимости (реализаций часто несколько), какой конструктор вызван и т.д. Совсем другое дело, когда зависимости компонентов прописаны явно, а не «размазаны» по коду.

Несомненно, бывают случаи, когда нам нужен каждый раз новый экземпляр компонента. Я в этом случае предпочитаю регистрировать фабрику компонентов (как инстанс) с кастомной логикой (которую всё-равно писать вручную), и использовать получение компонента через эту фабрику.

В случае необходимости отложенной инициализации (как я уже упоминал, для моих задач это редкость) можно попробовать использовать Lazy<>, причем в функторе инициализации придется использовать heavyComponentXTask.Result вместо await heavyComponentXTask

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

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

А с зависимостями, усложняющими граф самое вкусное. Достаточно описать задачу инициализации зависимости выше по коду чем задачи, где эта зависимость упоминается. Дальше задачи выстроятся в очередь планировщиком за счет использования await «автоматически».
Циклические зависимости компонентов друг от друга я не рассматриваю, т.к. это явные архитектурные проблемы.
Я вот только не понимаю, зачем вам вообще IoC-контейнер?

(отсутствие выигрыша при параллельном запуске легко может объясняться, например, тем, что у вас все обращения идут к одной БД, и она не справляется)

Что касается графа зависимостей: в вашем случае *нужно* знать порядок инициализации, в то время как при нормальном использовании контейнера он определяет его сам.
отсутствие выигрыша при параллельном запуске легко может объясняться, например, тем, что у вас все обращения идут к одной БД, и она не справляется
Если асинхронная инициализация отрицательно влияет на производительность, никто ведь не мешает часть компонентов проинициализировать по классической, синхронной схеме. Еще в рамках тюнинга конкретной ситуации можно внутри задачи someTask в самом начале явно прописать await someOtherTask — заставляя someOtherTask завершится до инициализации someTask, что в вашем случае положительно повлияет на производительность.

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

Сочетание этих факторов приводит к тому, что по факту вы не используете практически ничего из возможностей из контейнера, превращая вашу реализацию в poor man's DI.
Для тяжелых компонентов — синглтон практически единственный сценарий использования. Для всего остального ничто не мешает использовать IoC классически.
После чего у вас получается мешанина из классического и неклассического использования, в которой разобраться ничем не легче (и все равно надо помнить про порядок инициализации).
Я бы убрал добавление экземпляров в контейнер из метода инициализации и перенес его в метод FinishRegistrationTasks, значительно упростив логику и убрав синхронизацию.
Я так изначально и задумывал. Но вышло весьма «корявенько». Поэтому оставил вариант с синхронизацией.
Асинхронная регистрация — это довольно странный подход. Особенно, если решается задача асинхронной инициализации компонент. Почему бы не зарегистрировать компоненты отдельно, а уже затем в подходящий момент срезолвить все нужные компоненты и инициализировать их? В этом случае метод Initialize будет возвращать Task, компоненты можно срезолвить при старте приложения в нужном порядке, и вызвать соответствующие методы.

Плюсы:
1) мухи и котлеты будут отдельно — регистрация зависимостей занимается только своим делом.
2) инициализация будет асинхронной и за асинхронность отвечает сам компонент. Это чуть более правильно, так как Task — это не обязательно поток, вполне возможно, что внутри компонента происходят IO операции, которые можно запустить асинхронно.

Когда открываешь сравнительно простой чужой проект с точки зрения бизнес-логики и видишь в инициализации IoC-контейнера адскую мешанину из многоэтажных конструкций RegisterType, Resolve и InjectionConstructor, то… тоска и желание закрыть это и больше никогда не открывать. Очень тяжело разобраться сходу какая реализация интерфейса будет использоваться в качестве зависимости (реализаций часто несколько), какой конструктор вызван и т.д.

Это факт, что Unity имеет чрезмерно усложненный API, но здравомыслящие программисты обычно оборачивают его в нечто более понятное. Более того, если регистрация компонент сложна, состоит из каких-то хитро-закрученных кастомных регистраций, и компоненты имеют по двум-трем конструкторам, то это вероятнее всего сигнализирует о проблемах в архитектуре. Я работал с очень сложными проектами, где вся регистрация сводится по сути к чему-то вроде container.RegisterSingleton<SomeService, ISomeService>() или container.RegisterPerRequest<SomeService, ISomeService>().
Это хороший подход, мне уже говорили некоторые коллеги, что он более правилен архитектурно. Но он и рассчитан на более хорошую кодовую базу, которая имеется не всегда. Зачастую имеется куча кода (например легаси), который, в общем, не знает об асинхронности, потому что писался 5 лет назад, у которого вся инициализация в конструкторе, который выполняется секунд эдак 30, причем в качестве параметра ему предварительно надо вычислить текущую фазу луны. К нему надо писать дополнительный слой абстракции. Это всё, конечно, крайние случаи, но они показывают что в моем коде разобраться будет, в общем, проще, т.к. вся инициализация от создания до регистрации в контейнере — в одном месте.

Кроме того, непроинициализированный компонент в IoC-контейнере — потенциально опасное место.
до того, как нода попадет в балансировщик нагрузки

Если это как-то контроллируется, т.е. если нода не попадает в балансировщик, пока всё окончательно не проинициализируется, то ради чего усложнять? Какая разница, когда она попадёт в балансировщик (если, конечно, речь не о десятках минут инициализации)?
Естественно, если время старта устраивает, смысла нет. Я все это и затеял, когда суммарное время обновления нод (они обновляются по очереди) перевалило за час.
Sign up to leave a comment.

Articles