Стандартные «непонимашки»

Поверхностные возражения и реальные trade-off'ы

Эта страница — честный разбор типовых претензий, которые возникают при беглом знакомстве с Vortex. Не апологетика и не «вы ошибаетесь, потому что не понимаете». Для каждого пункта:

  • что в претензии валидно — где скептик прав;
  • где она неточна — что смягчает реальный механизм;
  • trade-off — какая цена за выбор такого решения.

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

Возражения сгруппированы в две категории:

  • Архитектурные — возникают при беглом просмотре кода, до реальной работы.
  • Практические — возникают через 2-3 недели работы, когда становятся видны эксплуатационные издержки.

Архитектурные возражения

1. «Это просто набор static singleton'ов»

«Vortex выглядит как GameManager.cs из туториала. Static facades повсюду, global state, антипаттерн.»

Что валидно. Vortex действительно строит API через статические шины: Inventory.AddItem(...), Audio.Play(...), Database.GetRecord<T>(...). Внешне это выглядит как global state.

Где неточно. За каждым статическим фасадом стоит подменяемый драйвер, выбираемый через DriverConfig-ассет. Внутри SystemController<T, TD> валидируется whitelist через DriversGenericList. Это не singleton с захардкоженной логикой, а тонкий стабильный API над подменяемой реализацией. Архитектурно — ближе к dependency inversion с relocated graph, чем к классическому глобальному состоянию.

Trade-off. Цена компактности вызовов (Inventory.AddItem(...) вместо _inventoryService.AddItem(...)) — статика выглядит глобальной, и культура team review должна это компенсировать. Если в команде нет дисциплины «не лезть в чужую модель напрямую», Vortex даст меньше формальных гарантий, чем DI.

См. подробнее: COMPARING, раздел про статические фасады.


2. «Невозможно понять зависимости класса»

«Когда я вижу Inventory.AddItem(...) внутри метода, я не могу сказать, от чего зависит этот класс. В DI достаточно посмотреть конструктор. В Vortex — нужно искать обращения к шинам по всему файлу.»

Что валидно. Это главный аргумент DI-мира против static-facade архитектур, и он описывает реальное свойство Vortex. Список зависимостей класса не виден через конструктор или поля — он распределён по обращениям к статическим шинам внутри методов.

Где неточно. Зависимости не скрыты — они расположены иначе. В DI они выражены через граф объектов (конструктор + регистрации в LifetimeScope). В Vortex — через assembly-граф (asmdef-references) и шинные контракты (известный публичный API на каждой шине). Понимание зависимостей класса требует знания архитектурного слоя, а не одного файла.

Конкретные приёмы навигации в Vortex:

  • asmdef-файл системы показывает, на какие сборки она ссылается — это полный список доступных шин.
  • IDE-поиск по Inventory. за секунды находит все места использования инвентаря — точнее, чем поиск по IInventoryService в DI (последний даёт и регистрации, и моки, и сам интерфейс).
  • Регулярное выражение [A-Z]\w+\.\w+\( находит все cross-system вызовы в файле.

Trade-off. Compactness call sites + workflow readability ценой того, что зависимости класса не локализованы в одной точке. Vortex принимает читаемость workflow важнее, чем explicit граф зависимостей для каждого отдельного класса. Это сознательный обмен: vortex'овский код читается как последовательность доменных действий, DI-код — как набор инжекций + методы.


3. «Без DI невозможно тестировать»

«Раз нет constructor injection — невозможно подменить зависимости в юнит-тестах. Покрытие тестами заведомо хуже.»

Что валидно. В Vortex нельзя написать классический юнит-тест handler'а с new GiveItemHandler(mockInventory) — потому что зависимости приходят не через конструктор, а через статические шины.

Где неточно. Vortex использует другой механизм inversion-of-control. Подмена реализации идёт через Driver pattern: InventoryDriverTest : IInventoryDriver указывается в тестовом DriverConfig-ассете, Vortex поднимает систему с моком. Это не «невозможно тестировать», это другой стиль тестов — integration-style playmode-тестирование вместо isolated-style юнитов.

Trade-off. Тесты в Vortex дороже одного запуска (нужен playmode), но дешевле в покрытии бизнес-сценариев (один тест прогоняет всю цепочку). Если команда привыкла к unit-тестам через конструктор-моки — переход на playmode-стиль требует адаптации.

См. подробнее: COMPARING, раздел «Тестируемость».


4. «Слишком много reflection и runtime composition»

«Activator.CreateInstance, type scanning, string-based controller IDs, auto-registration. Это хрупко, небезопасно, неотлаживаемо.»

Что валидно. Vortex действительно использует рефлексию в нескольких местах: type scanning для dropdown'ов в инспекторе, DriversGenericList whitelist, ComplexModel partial-сборка, template generation. String-based binding в MiniGames (ControllerType = "MyGameController") даёт классический риск «rename сломает всё».

Где неточно. Reflection происходит только в editor-time или один раз на старте сцены, не в hot path игрового цикла. Подмена контроллера через строку — не «магия», а inspector-native composition для дизайнерского workflow: дизайнер выбирает в dropdown'е, что физически работает. Rename ловится через [MovedFrom]-атрибуты Unity при миграции.

Trade-off. Получаем designer-friendly композицию ценой того, что часть связности не видна компилятору. Для проектов без дизайнерского участия (technical demos, утилиты) этот trade-off менее выгоден.

См. подробнее: COMPOSITION.


5. «ReactiveValues — велосипед вместо UniRx»

«Зачем писать свой reactive layer, когда есть готовые UniRx, R3, System.Reactive?»

Что валидно. Если задача — построить pipeline-обработку потоков событий (Throttle, Debounce, CombineLatest, FlatMap), UniRx это делает лучше: ~80 операторов из коробки, проверенная библиотека.

Где неточно. ReactiveValue<T> решает другую задачу. Это не FRP-библиотека и не Rx-stream абстракция, а mutation-controlled state primitive с owner-key capability. У UniRx нет эквивалента «контейнера со встроенной защитой от посторонней мутации» — BehaviorSubject<T> похож по поведению, но без owner-механизма. Vortex и Rx сравниваются не по тем же осям.

Trade-off. ReactiveValue минималистичен (одна задача, одно событие, owner). Если в проекте нужна сложная pipeline-обработка (input pipelines, network streams) — Rx можно подключить поверх, не вместо. Не конкуренты, а разные уровни.

См. подробнее: OWNERSHIP, раздел «vs Rx».


6. «Owner key — security theater»

«Set(value, owner) — это не настоящая защита. Через reflection всё ломается. Любой может передать любой owner.»

Что валидно. Owner-механизм действительно не защищает от malicious code. Если кто-то откроет приватное поле через рефлексию или специально сделает Hp.SetOwner(externallyVisibleObject) — защита снимается. Это не криптография и не sandbox.

Где неточно. Назначение owner-механизма — не защита от злонамеренного кода, а архитектурный замок против случайных нарушений. Когда в команде из 10 человек кто-то по невнимательности пишет model.Hp.Set(0) мимо контроллера — компилятор разрешит, но runtime отклонит мутацию с логом ошибки. Капабилити — это маркер дисциплины, читаемый в коде как «эта мутация принадлежит этому контроллеру».

Trade-off. Снижение accidental erosion ценой синтаксического шума (Set(value, _key) вместо Set(value)). Для опытной команды с culture of review шум может казаться избыточным. Для большой команды или долгоживущего проекта — полезный sanity check.

См. подробнее: OWNERSHIP, раздел «Капабилити через owner-key».


7. «Архитектура держится на conventions, а не компиляторе»

«Правила вроде "View не пишет в model" не enforce'ятся компилятором. Если правило нельзя проверить автоматически — это плохая архитектура.»

Что валидно. Часть правил Vortex действительно держится на дисциплине, не на типах. Например, «UI вызывает контроллер, не мутирует модель напрямую» — компилятор разрешит и то и другое, нарушение ловится только на code review.

Где неточно. Vortex не «полностью convention-driven». Значительная часть правил enforce'ена структурно:

  • Слоистая иерархия — через asmdef-границы (Core не видит Unity, Unity не видит Sdk).
  • Driver whitelist — через DriversGenericList, посторонний драйвер runtime'но отклоняется.
  • Owner-key — препятствует случайной мутации.
  • internal-emit на шинах — закрывает право триггерить события извне сборки.
  • [SerializeReference] + [ClassFilter] — визуально подсвечивает несовместимые типы.

То что остаётся на конвенциях — это специфическое поведенческое разделение Controller / Data / View. Но это уже не «вся архитектура», а узкая зона ответственности дисциплины.

Trade-off. Vortex принимает, что культура review — реальный инструмент защиты архитектуры в небольших и средних командах. В больших организациях с слабой review-культурой это становится проблемой — там DI с compile-time гарантиями надёжнее.

См. подробнее: COMPARING, раздел «Природа ограничителя».


8. «Half-Vortex невозможен — система слишком взаимосвязана»

«Если игнорировать owner-ключи / обходить шины / мутировать чужие данные — Vortex деградирует. Значит архитектура хрупкая.»

Что валидно. Это правда. Vortex — это интегрированная доктрина, и обход одного правила обрушивает целостность остальных. Половинчатое применение не работает.

Где неточно. «Хрупкость» в этом смысле — обратная сторона целостности. Любая системно построенная архитектура хрупка к избирательным нарушениям: DI без constructor injection — это не DI; SOLID с тремя из пяти принципов — это не SOLID; Redux с прямыми мутациями — это не Redux. Vortex не уникален в этом отношении, он просто более интегрирован, и поэтому нарушения видны быстрее.

Trade-off. Цена tightly-integrated doctrine — нужна команда, готовая принять её целиком. Если в команде есть человек, который «делает как удобно», то любая sister-доктрина деградирует, и Vortex не исключение. Альтернатива — менее опинионированный фреймворк, где локальные нарушения не каскадируют так быстро.

См. подробнее: COMPOSITION, раздел «Autonomous packages».


9. «Слишком Unity-specific»

«Это привязано к Unity по самые уши. Невозможно использовать вне движка. ScriptableObject, MonoBehaviour, Editor pipeline — всё это лишает портативности.»

Что валидно. Полностью валидная претензия. Vortex сознательно проектировался под Unity production pipeline. ScriptableObject-конфиги, Inspector-driven composition, EditorTools-атрибуты, prefab-driven views — это всё неперносимо.

Где неточно. Только частично. Core-слой (ru.vortex.system, ru.vortex.database, ru.vortex.app, и др.) — это чистый C#, без Unity-зависимостей. Его можно вынести в консольное приложение, серверный backend, Blazor — и он работает. Но это узкая часть фреймворка; основная ценность Vortex в Unity-слое и выше.

Trade-off. Vortex обменивает универсальность на глубокую интеграцию с Unity workflow. Если нужен переносимый между движками фреймворк — Vortex не подходит, и это намеренный выбор, не недосмотр. Generic DI (VContainer, Zenject) даёт переносимость, но не даёт inspector-native composition.

См. подробнее: PIPELINE.


Практические возражения

Эти претензии возникают не при беглом чтении кода, а в ходе реальной работы. Они менее философские и более эмпирические.

10. «Высокий порог входа»

«Static buses + Driver pattern + GUID везде + ScriptableObject-конфиги + EditorTools-атрибуты + слои + asmdef — это пять концепций одновременно. Новый разработчик тонет в первые дни.»

Что валидно. Полностью валидно. Vortex — opinionated runtime с собственным каноном, и полу-Vortex не работает (см. пункт 7). Это требует принять модель целиком, что для нового человека — значительный когнитивный груз.

Где неточно. Сложность концепций неравнозначна. После принятия 2-3 базовых принципов (Controller/Data/View, шинная развязка, GUID на типовые единицы) остальное складывается. Документ WELCOME специально структурирован для быстрого старта, мета-страницы — для последующего углубления.

Trade-off. Большой объём концепций upfront окупается скоростью добавления новых фич после освоения. Команда из 5 человек на 6-м месяце работы добавляет фичи в 3-5 раз быстрее, чем команда из 5 человек на классическом коде без фреймворка — потому что инфраструктура уже на месте, нужно только дописать логику.

Совет: новый разработчик в Vortex-проекте должен первую неделю работать в паре с опытным. После этого «всё встаёт на место».


11. «Editor может тормозить»

«Auto-creation ассетов на первой компиляции + Odin-drawer'ы + ValueSelector-резолверы + type scanning — Editor становится медленным.»

Что валидно. Это наблюдается на практике. Особенно заметно:

  • При первом открытии Editor после Reimport AllCoreAssetsController создаёт SO-ассеты, SettingsDriver создаёт пресеты, type scanning крутится.
  • При открытии конфигов с десятками ValueSelector-полей — резолверы могут тормозить, если данные тяжёлые.
  • При работе с большими [SerializeReference]-массивами под Odin'ом — рендеринг инспектора заметно медленнее ванильного Unity.

Где неточно. Большинство проблем — точечные, не системные. Vortex включает специфические мехнизмы для этих случаев (TTL-кеш в ValueSelector, lazy initialization в drawer'ах). Большие тормоза обычно связаны не с самим Vortex, а с конкретными конфигами (тяжёлые ValueSelector-резолверы, мегабайтные [SerializeReference]-массивы).

Trade-off. Designer-friendly editor стоит CPU. Если в проекте Editor performance критична (большой проект, частая работа с конфигами) — стоит профилировать и оптимизировать конкретные точки, не отказываться от Vortex целиком.


12. «Odin как обязательная платная зависимость»

«Vortex требует Odin Inspector ($55). Это блокирует команды, которые не могут / не хотят покупать ассеты. Плюс Odin сейчас проходит через смену собственника — есть риск.»

Что валидно. Полностью валидно. Odin Inspector — обязательная зависимость Vortex. Без неё половина EditorTools-атрибутов не работает. Плата $55 — небольшая, но обязательная, и для образовательных / open-source проектов это барьер. Смена собственника Odin (от Sirenix к Daniweb) — реальный supply chain risk.

Где неточно. Зависимость осознанная и проговоренная в документации. Это не «случайно потащили в зависимости», а сознательный выбор: Vortex использует Odin для production pipeline, и пытаться эмулировать половину Odin своими силами было бы дорого и хрупко. Альтернатива — переход на бесплатные альтернативы (Tri Inspector, NaughtyAttributes) — теоретически возможна, но требует портирования всех Vortex-drawer'ов под другой API.

Trade-off. Vortex принимает зависимость от внешнего инструмента ради качества production pipeline. Если для проекта критична бесплатность — Vortex в текущем виде не подходит. Если Odin недоступен по причинам политики компании — выбор тот же.


13. «Static state дрейфует между Editor-сессиями»

«Static singletons сохраняют состояние между Play-сессиями в Editor. Без явного reset это даёт дрейф в тестах и непредсказуемое поведение.»

Что валидно. Это реальная проблема статических архитектур в Unity Editor. Singleton<T>._instance может пережить выход из Play Mode, если не был явно очищен. Так же шины с подписками: event Action OnRewardGiven может накапливать stale-подписчиков от прошлых сессий.

Где неточно. Vortex имеет встроенные механизмы reset:

  • Singleton<T>.Dispose() — очистка инстанса.
  • SystemController<T, TD>.OnDriverDisconnect — отключение драйвера при смене.
  • App.OnExit event — точка для подписки на сброс.
  • [RuntimeInitializeOnLoadMethod] — переинициализация при заходе в Play.

При дисциплинированном использовании дрейф не возникает. Проблема более актуальна для проектов с долгими Editor-сессиями и интенсивной работой с тестами.

Trade-off. Цена компактности static API — обязательность дисциплины reset. В DI-архитектуре каждый запуск Play создаёт новый container, проблема не существует. В Vortex App.OnExit нужно обрабатывать явно. Решается дисциплиной.


14. «Reinventing the wheel — для каждой подсистемы есть внешний аналог»

«ReactiveValues → UniRx. DriverConfig → DI-container. ComplexModel → IoC. ExtLogic → commands/events. MiniGames → state machine framework. SerializeController → Newtonsoft. Зачем всё своё?»

Что валидно. Каждая подсистема Vortex имеет внешний аналог в индустрии, часто более зрелый и шире протестированный.

Где неточно. Vortex строит не набор loosely-coupled libraries, а единую экосистему с одним workflow и одной архитектурной философией. Когда ReactiveValue<T> интегрирован с ISaveable, Database, IReactiveData-подписками квестов, [POCO]-сериализацией через SerializeController — это не просто «обёртка над int», это узел системы. Замена ReactiveValue на UniRx разорвёт пять интеграционных связей одновременно.

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

Trade-off. Vortex обменивает «лучшее в каждом узком классе» на «согласованное в целом». Если в проекте критична специализация одной подсистемы (например, нужна мощнейшая reactive-обработка для специфической задачи) — лучше взять чистый UniRx как стандалон, без Vortex. Если критично общее качество production pipeline — Vortex даёт лучший результат.

Главное. Vortex оптимизирует не качество отдельного инструмента, а стоимость согласования всей production architecture. Это не попытка написать лучший reactive layer, лучший serializer или лучший DI — это попытка сделать так, чтобы 20 подсистем не требовали ручной интеграции друг с другом.


Когда Vortex — плохой выбор

Этот раздел — не оборона. Это честный список ситуаций, в которых Vortex не подходит и стоит выбрать что-то другое.

  • Server-authoritative бэкенд с in-process multi-tenancy. Static buses можно партиционировать по world-key или через AsyncLocal context, и sharding (один процесс на мир) работает без проблем. Что действительно не ложится: Sdk-контроллеры (GameController, QuestController) спроектированы под одну сессию и потребуют переписывания scoping-логики; client-side подсистемы (Save/Settings/Loader/Database драйверы) на сервере нерелевантны. Если архитектура — один процесс на шард, Vortex применим. Если требуется in-process multi-tenancy с тысячами одновременных миров — преимущества фреймворка теряются, потому что ambient context подменяет явность шины.
  • Multi-runtime приложения. Если проект должен работать одновременно в Unity и вне Unity (CLI-инструменты, WebAssembly, headless-симуляция) — Vortex портится только Core-слоем, Unity-инфраструктура остаётся за бортом.
  • Enterprise-style service decomposition. Если архитектурный канон команды требует strict compile-time governance, sealed boundaries, formal contracts между сервисами — Vortex с его культурой review и runtime композицией не пройдёт review.
  • Engine portability. Vortex привязан к Unity до уровня инспектора и asmdef. Переезд на Godot / Unreal — фактически переписывание с нуля.
  • Команды без shared architectural discipline. Vortex полагается на review-культуру для части правил. В командах с высокой текучкой / без сильного тех-лида канон будет деградировать быстрее, чем приживаться.
  • Tooling, несовместимый с Odin. Если по политике компании / лицензионным причинам / личным предпочтениям Odin Inspector неприемлем — Vortex в текущем виде использовать нельзя.
  • Микропроекты на 1-2 экрана. Жанровые прототипы, jam-проекты, технические демо: overhead Vortex-инфраструктуры не окупится — быстрее написать как угодно без фреймворка.

Если ваш проект попадает в один из этих кейсов — Vortex не «плохой», а не для этого. Каждый фреймворк имеет зону эффективности; за её пределами стоит брать инструмент, оптимизированный под другие задачи.


Что под капотом всех trade-off'ов

Если посмотреть на 14 пунктов одним взглядом, видна общая ось: Vortex обменивает универсальность и compile-time строгость на компактность, дисциплину и production-pipeline качество.

Vortex выигрывает в:                  Платит за это в:
─────────────────────────             ────────────────────────
• компактность API                    • культура review нужна
• inspector-native workflow           • Odin как зависимость
• designer-friendly composition       • Unity-specific
• скорость добавления фич             • высокий порог входа
• целостность системы                 • half-Vortex не работает
• unified ecosystem                   • reinventing wheel

Vortex — не попытка заменить все архитектурные подходы. Это фреймворк для команд, готовых обменять часть compile-time строгости и универсальности на:

  • высокую скорость production,
  • inspector-native workflow,
  • целостную runtime architecture,
  • минимизацию интеграционного клея между подсистемами.

Если эти ценности совпадают с проектом — Vortex становится очень эффективным. Если нет — его ограничения начнут ощущаться раньше преимуществ.

Это не аргумент в защиту Vortex, а honest disclosure: «вот что мы покупаем и чем платим». Любой опытный архитектор смотрит на фреймворк именно через такую призму.


Что делать с этим списком

Если вы оцениваете Vortex для своего проекта. Пройдите по 14 пунктам и отметьте, какие из «валидных» частей критичны для вас. Если 2-3 — overhead приемлем. Если 6+ — стоит рассмотреть альтернативы. Раздел «Когда Vortex плохой выбор» — отдельный быстрый чек-лист на dealbreaker'ы.

Если вы новый разработчик в Vortex-проекте. Этот список — прививка от типовых заблуждений. Вы увидите static facades, рефлексию, owner-key — и не будете автоматически реагировать «это плохо». Понимание trade-off'ов даёт способность работать в рамках фреймворка, не борясь с ним.

Если вы оцениваете Vortex как преподаватель / архитектор. Список показывает, что Vortex — это не серебряная пуля, а выбор позиции на оси «универсальность ↔ интеграция». Это легитимная архитектурная школа со своими сильными и слабыми сторонами.


Связанные страницы: