Сам анализ быстрый, его время несущественное по сравнению с компиляцией. У нас Swift файлы компилируются с нуля за порядка 10-20 минут, при этом чистое время работы анализатора не превышает нескольких секунд. В инкрементальной сборке так же, время работы анализатора не больше 1%.
Однако тут есть и ложка дегтя — статический анализ Implicits зависит от swift-syntax, а она привносит свои недостатки. Многие проекты уже столкнулись с этими проблемами когда внедряли к себе макросы – компиляция swift-syntax довольно долгая, и при этом она компилируется без оптимизаций и долго парсит код.
Сейчас для решения этой проблемы есть экспериментальные флажки. Фича хоть и экспериментальная, но по-моему абсолютно необходимая если вы используете в проекте swift-syntax.
Надо сказать что в нашем проекте мы используем нестандартную для iOS мира билд систему — GN, и имеем большой простор для кастомизации, поэтому мы давно имеем возможность поставлять себе предсобранный в релизе swift-syntax, и экономили многие минуты, или даже десятки минут сборки.
Также можно заметить, что Implcits могут и уменьшить время сборки. Во-первых, из-за уменьшения количества кода, что напрямую означает меньшую работу компилятора. Во-вторых, промежуточные, "прокидывающие" модули перестают зависеть от модулей с прокидываемыми сущностями:
Видно что импорта больше нет, а значит меньше работы компилятору. Мы конечно не замеряли время компиляции которое сэкономили, потому что оно равномерно размазывается по всему коду и по времени внедрения, и замерить это очень тяжело.
Передача зависимостей через параметры позволяет безопасно предоставить конкретную зависимость в момент инициализации объекта, при этом компилятор проверяет типы, а зависимость остаётся константной.
То, что вы описываете дальше – это по сути синглтоны и другие глобальные хранилища. Нам в команде кажется, что это не лучший подход, но это тема для отдельной дискуссии. На просторах интернета много материалов о плюсах и минусах такого решения.
А не могли бы вы поделится этим багом? Implicits хорошо покрыты тестами, в том числе и для concurrency, можно посмотреть тут и донести пр/issue если каких то тесткейсов не хвататет или в каких то кейсах не работает.
Но в проекте с сотнями или скорее тысячами синглтонов это становится невероятно хрупким. Нужно следить за порядком инициализации всех этих static var, помнить какой configure() должен быть вызван раньше какого, не забывать переинициализировать всё в тестах. А force unwrap (знак !) это креш в рантайме вместо ошибки компиляции при неправильном порядке инициализации.
Например, в Chromium столкнулись с похожей проблемой. У них сотни сервисов, и их документация прямо говорит: "we can not rely on manual ordering when we have so many components.". Поэтому они построили систему, https://www.chromium.org/developers/design-documents/profile-architecture/, которая автоматически строит граф зависимостей и резолвит порядок инициализации.
Подход легитимный, но в простом исполнении очень хрупкий. А чтобы убрать хрупкость, требуется большая работа.
Вы не поверите, но когда имплиситы были лишь одной из идей, как решить проблему DI, одной из конкурирующих идей был именно Parameter Object. Причём с полной автоматизацией: автоматическая генерация самих Params-структур и автоматическая генерация инициализаторов для конверсии одного Parameter Object в другой. У нас даже были прототипы.
Мы не выбрали этот подход по нескольким причинам:
Кодогенерация конверсий между типами параметров технически сложна и хрупка
У каждой функции обычно свой набор аргументов: buildContainer принимает одно, buildList другое, buildCell третье. Паттерн хорошо работает, когда на нескольких уровнях нужен одинаковый набор параметров, но это редкость
Этот подход менее агностичен к стилю программирования и диктует как структурировать код вокруг Params-типов. Имплиситы работают с любым существующим кодом без переписывания архитектуры
А если использовать Parameter Object без автоматизации, кода становится больше. В общем случае нужно создать структуру для каждого уровня, дать ей имя, написать инициализатор, extension для конверсии в следующий тип, а этот extension по размеру такой же, как само прокидывание.
При этом Parameter Object хороший паттерн, мы его активно используем там, где он уместен. Например, когда глубоко в иерархии создаётся большой объект и через много слоёв нужно прокинуть одни и те же параметры. И кстати, Parameter Object отлично сочетается с имплиситами: можно с помощью имплиситов создать Parameter Object и прокинуть его куда надо. Эти подходы друг другу не противоречат.
Да, добавить liveValue в корневом (или любом другом, где "видно" реализацию и ключ) модуле можно, но это антипаттерн по двум причинам: во-первых, если забыть это сделать, то в проде будет использоваться testValue, во-вторых, если вдруг будет два конформанса в разных модулях с разными liveValue то получится неуточнённое поведение (unspecified behavior), какая реализация выберется, зависит от реализации компилятора, линковщика и их флагов сборки. А если будет, например, зависеть от конфигурации сборки, в релизе будет выбираться одна реализация, а в дебаге другая? Интересное приключение будет это дебажить!
Дело тут не в том что мы пишем конкретно браузер, хотя сложность проекта тоже влияет, но скорее дело в стандартах качества, в соблюдении архитектурных паттернов и стремлении отловить ошибки на этапе сборки. Величина кодовой базы, команды и аудитории приложения является мультипликатором, с ростом масштаба требования к качеству и стабильности растут.
Спасибо за развёрнутый комментарий, давайте попробуем разобраться.
Важно понимать, что Dependencies в том виде, в котором библиотека предполагается к использованию, не реализует принцип dependency inversion, а является удобной обёрткой для синглтонов, контейнером глобальных переменных с механизмом переопределения. Это легко проверить: попробуйте заинжектить в модуль А реализацию из модуля Б так, чтобы А не зависел от Б, и вы увидите что liveValue должен быть определён при объявлении ключа, а значит зависимость на конкретный тип никуда не девается.
Если же пытаться использовать Dependencies для настоящего dependency inversion, то есть ставить заглушки как дефолтные значения и переопределять всё честно через withDependencies, то опыт получается не очень: не видно какие зависимости нужно переопределять, они скрыты внутри вызываемого кода, и если что-то забыли — узнаете только в рантайме. По сути получаются те же имплиситы, только без статического анализатора и с менее удобным API.
Point-Free делают классные инструменты, и Dependencies — продуманный компромисс между удобством и тестируемостью, который для многих проектов отлично подходит, просто для нас он не подошёл. Говорить о пользе dependency inversion выходит за рамки статьи, но утверждать что он не нужен 95% приложений — всё-таки смелое заявление :)
Спасибо за вопрос.
Сам анализ быстрый, его время несущественное по сравнению с компиляцией. У нас Swift файлы компилируются с нуля за порядка 10-20 минут, при этом чистое время работы анализатора не превышает нескольких секунд. В инкрементальной сборке так же, время работы анализатора не больше 1%.
Однако тут есть и ложка дегтя — статический анализ Implicits зависит от swift-syntax, а она привносит свои недостатки. Многие проекты уже столкнулись с этими проблемами когда внедряли к себе макросы – компиляция swift-syntax довольно долгая, и при этом она компилируется без оптимизаций и долго парсит код.
Сейчас для решения этой проблемы есть экспериментальные флажки. Фича хоть и экспериментальная, но по-моему абсолютно необходимая если вы используете в проекте swift-syntax.
Надо сказать что в нашем проекте мы используем нестандартную для iOS мира билд систему — GN, и имеем большой простор для кастомизации, поэтому мы давно имеем возможность поставлять себе предсобранный в релизе swift-syntax, и экономили многие минуты, или даже десятки минут сборки.
Также можно заметить, что Implcits могут и уменьшить время сборки. Во-первых, из-за уменьшения количества кода, что напрямую означает меньшую работу компилятора. Во-вторых, промежуточные, "прокидывающие" модули перестают зависеть от модулей с прокидываемыми сущностями:
И если использовать имплиситы:
Видно что импорта больше нет, а значит меньше работы компилятору.
Мы конечно не замеряли время компиляции которое сэкономили, потому что оно равномерно размазывается по всему коду и по времени внедрения, и замерить это очень тяжело.
Спасибо за вопрос!
Передача зависимостей через параметры позволяет безопасно предоставить конкретную зависимость в момент инициализации объекта, при этом компилятор проверяет типы, а зависимость остаётся константной.
То, что вы описываете дальше – это по сути синглтоны и другие глобальные хранилища. Нам в команде кажется, что это не лучший подход, но это тема для отдельной дискуссии. На просторах интернета много материалов о плюсах и минусах такого решения.
А не могли бы вы поделится этим багом?
Implicits хорошо покрыты тестами, в том числе и для concurrency, можно посмотреть тут и донести пр/issue если каких то тесткейсов не хвататет или в каких то кейсах не работает.
Можно, конечно. Технически это работает.
Но в проекте с сотнями или скорее тысячами синглтонов это становится невероятно хрупким. Нужно следить за порядком инициализации всех этих static var, помнить какой configure() должен быть вызван раньше какого, не забывать переинициализировать всё в тестах. А force unwrap (знак
!) это креш в рантайме вместо ошибки компиляции при неправильном порядке инициализации.Например, в Chromium столкнулись с похожей проблемой. У них сотни сервисов, и их документация прямо говорит: "we can not rely on manual ordering when we have so many components.". Поэтому они построили систему, https://www.chromium.org/developers/design-documents/profile-architecture/, которая автоматически строит граф зависимостей и резолвит порядок инициализации.
Подход легитимный, но в простом исполнении очень хрупкий. А чтобы убрать хрупкость, требуется большая работа.
Вы не поверите, но когда имплиситы были лишь одной из идей, как решить проблему DI, одной из конкурирующих идей был именно Parameter Object. Причём с полной автоматизацией: автоматическая генерация самих Params-структур и автоматическая генерация инициализаторов для конверсии одного Parameter Object в другой. У нас даже были прототипы.
Мы не выбрали этот подход по нескольким причинам:
Кодогенерация конверсий между типами параметров технически сложна и хрупка
У каждой функции обычно свой набор аргументов: buildContainer принимает одно, buildList другое, buildCell третье. Паттерн хорошо работает, когда на нескольких уровнях нужен одинаковый набор параметров, но это редкость
Этот подход менее агностичен к стилю программирования и диктует как структурировать код вокруг Params-типов. Имплиситы работают с любым существующим кодом без переписывания архитектуры
А если использовать Parameter Object без автоматизации, кода становится больше. В общем случае нужно создать структуру для каждого уровня, дать ей имя, написать инициализатор, extension для конверсии в следующий тип, а этот extension по размеру такой же, как само прокидывание.
При этом Parameter Object хороший паттерн, мы его активно используем там, где он уместен. Например, когда глубоко в иерархии создаётся большой объект и через много слоёв нужно прокинуть одни и те же параметры. И кстати, Parameter Object отлично сочетается с имплиситами: можно с помощью имплиситов создать Parameter Object и прокинуть его куда надо. Эти подходы друг другу не противоречат.
Хорошо, понял ваш посыл, было интересно обсудить!
Это замечательно, значит что у вас проблем, которые надо решать нет :)
Да, добавить
liveValueв корневом (или любом другом, где "видно" реализацию и ключ) модуле можно, но это антипаттерн по двум причинам: во-первых, если забыть это сделать, то в проде будет использоватьсяtestValue, во-вторых, если вдруг будет два конформанса в разных модулях с разнымиliveValueто получится неуточнённое поведение (unspecified behavior), какая реализация выберется, зависит от реализации компилятора, линковщика и их флагов сборки. А если будет, например, зависеть от конфигурации сборки, в релизе будет выбираться одна реализация, а в дебаге другая? Интересное приключение будет это дебажить!Дело тут не в том что мы пишем конкретно браузер, хотя сложность проекта тоже влияет, но скорее дело в стандартах качества, в соблюдении архитектурных паттернов и стремлении отловить ошибки на этапе сборки. Величина кодовой базы, команды и аудитории приложения является мультипликатором, с ростом масштаба требования к качеству и стабильности растут.
Спасибо за развёрнутый комментарий, давайте попробуем разобраться.
Важно понимать, что Dependencies в том виде, в котором библиотека предполагается к использованию, не реализует принцип dependency inversion, а является удобной обёрткой для синглтонов, контейнером глобальных переменных с механизмом переопределения. Это легко проверить: попробуйте заинжектить в модуль А реализацию из модуля Б так, чтобы А не зависел от Б, и вы увидите что
liveValueдолжен быть определён при объявлении ключа, а значит зависимость на конкретный тип никуда не девается.Если же пытаться использовать Dependencies для настоящего dependency inversion, то есть ставить заглушки как дефолтные значения и переопределять всё честно через
withDependencies, то опыт получается не очень: не видно какие зависимости нужно переопределять, они скрыты внутри вызываемого кода, и если что-то забыли — узнаете только в рантайме. По сути получаются те же имплиситы, только без статического анализатора и с менее удобным API.Point-Free делают классные инструменты, и Dependencies — продуманный компромисс между удобством и тестируемостью, который для многих проектов отлично подходит, просто для нас он не подошёл. Говорить о пользе dependency inversion выходит за рамки статьи, но утверждать что он не нужен 95% приложений — всё-таки смелое заявление :)