Обновить
4
22
Альфред Зиен@AlfredZien

iOS dev

Отправить сообщение

Спасибо за вопрос.

Сам анализ быстрый, его время несущественное по сравнению с компиляцией. У нас Swift файлы компилируются с нуля за порядка 10-20 минут, при этом чистое время работы анализатора не превышает нескольких секунд. В инкрементальной сборке так же, время работы анализатора не больше 1%.

Однако тут есть и ложка дегтя — статический анализ Implicits зависит от swift-syntax, а она привносит свои недостатки. Многие проекты уже столкнулись с этими проблемами когда внедряли к себе макросы – компиляция swift-syntax довольно долгая, и при этом она компилируется без оптимизаций и долго парсит код.

Сейчас для решения этой проблемы есть экспериментальные флажки. Фича хоть и экспериментальная, но по-моему абсолютно необходимая если вы используете в проекте swift-syntax.

Надо сказать что в нашем проекте мы используем нестандартную для iOS мира билд систему — GN, и имеем большой простор для кастомизации, поэтому мы давно имеем возможность поставлять себе предсобранный в релизе swift-syntax, и экономили многие минуты, или даже десятки минут сборки.

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

import FooServiceModule

struct IntermediateFactory {
  init(fooService: FooService) {
     ... = BarService(fooService: fooService)
  }
}

И если использовать имплиситы:

struct IntermediateFactory {
  init(_ scope: ImplicitScope) {
     ... = BarService(scope)
  }
}

Видно что импорта больше нет, а значит меньше работы компилятору.
Мы конечно не замеряли время компиляции которое сэкономили, потому что оно равномерно размазывается по всему коду и по времени внедрения, и замерить это очень тяжело.

Спасибо за вопрос!

Передача зависимостей через параметры позволяет безопасно предоставить конкретную зависимость в момент инициализации объекта, при этом компилятор проверяет типы, а зависимость остаётся константной.

То, что вы описываете дальше – это по сути синглтоны и другие глобальные хранилища. Нам в команде кажется, что это не лучший подход, но это тема для отдельной дискуссии. На просторах интернета много материалов о плюсах и минусах такого решения.

А не могли бы вы поделится этим багом?
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 в другой. У нас даже были прототипы.

Мы не выбрали этот подход по нескольким причинам:

  1. Кодогенерация конверсий между типами параметров технически сложна и хрупка

  2. У каждой функции обычно свой набор аргументов: buildContainer принимает одно, buildList другое, buildCell третье. Паттерн хорошо работает, когда на нескольких уровнях нужен одинаковый набор параметров, но это редкость

  3. Этот подход менее агностичен к стилю программирования и диктует как структурировать код вокруг 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% приложений — всё-таки смелое заявление :)

Информация

В рейтинге
300-й
Откуда
Белград, Сербия
Зарегистрирован
Активность