Мы рады открыть исходный код совершенно новой системы управления зависимостями для приложений на Swift. Она позволяет легко распространять зависимости вглубь вашего приложения эргономичным, но в то же время безопасным способом. Как только вы начнете управлять ими, то сразу же сможете писать более простые тесты, обнаруживать в превью Xcode мощь, не имеющую себе равных, улучшать время компиляции и многое другое.

Присоединяйтесь к нам, чтобы получить краткий обзор и подумать над добавлением библиотеки в свое приложение уже сегодня!

Обзор

Зависимости — это типы и функции в вашем приложении, которым нужно взаимодействовать с внешними системами, которые вы не контролируете. Классическими примерами этого являются API-клиенты, делающие сетевые запросы к серверам; но и безвредные на вид вещи, такие как инициализаторы UUID и Date, доступ к файлам, пользовательские настройки по умолчанию, и даже часы и таймеры - все это можно рассматривать как зависимости.

Вы можете уйти очень далеко в разработке приложений, даже не задумываясь об управлении зависимостями (или, как некоторые любят это называть, «внедрении зависимостей»), но в итоге неконтролируемые зависимости способны вызвать множество проблем в вашей кодовой базе и цикле разработки:

  • Неконтролируемые зависимости затрудняют написание быстрых детерминированных тестов, т.к. вы будете восприимчивы к капризам внешнего мира, таким как файловые системы, сетевое подключение, скорость интернета, время безотказной работы сервера и многое другое.

  • Многие зависимости плохо работают в превью SwiftUI, таких как менеджеры геолокации и распознаватели речи, а некоторые не работают даже в симуляторах, таких как менеджер движения и т. д. Что лишает вас возможности легко повторить дизайн технических характеристик этих фреймворков, если вы их используете.

  • Зависимости, которые взаимодействуют со сторонними библиотеками, не принадлежащими Apple (например, Firebase, библиотеки веб-сокетов, сетевые библиотеками, библиотеками потокового видео и т. д.), как правило, много весят и дольше компилируются. Что способно замедлить цикл вашей разработки.

По этим и многим другим причинам, крайне рекомендуем вам взять ваши зависимости под контроль, а не позволять им управлять вами.

Но управление зависимостью — это лишь начало. Как только вы берете их под контроль, вы сталкиваетесь с целым набором новых проблем:

  • Как вы распространите зависимости по всему приложению таким способом, чтобы это было более эргономично, чем явно передавать их повсюду, при этом - безопаснее, чем иметь глобальную зависимость?

  • Как вы переопределите зависимости только для одной части вашего приложения? Это может быть сподручно(*удобно) - переопределять зависимости в тестах и в превью SwiftUI, а также в процессах, относящихся к пользователям, таких как введение в продукт.

  • Как вы можете быть уверены, что переопределили все зависимости, используемые функцией в тестах? Было бы неправильно одни зависимости подделать для теста, но другие оставить открытыми для взаимодействия с внешним миром.

Эта библиотека решает все вышеперечисленные вопросы и многое, многое другое.

Применение вашей первой зависимости

Библиотека позволяет вам регистрировать ваши собственные зависимости, но она также предоставляет множество управляемых зависимостей прямо “из коробки" (полный список см. в DependencyValues), и есть большая вероятность, что вы сразу же сможете применить одну из них. Если вы используете планировщики Date(), UUID(), Task.sleep или Combine непосредственно в логике вашей функции, то вы уже можете начать использовать эту библиотеку.

Любое место, где вы используете одну из этих зависимостей непосредственно в логике функции, не передавая её в функцию явно, может быть обновлено с помощью обертки свойств @Dependency, чтобы сначала объявить вашу зависимость в функции:

final class FeatureModel: ObservableObject {
  @Dependency(\.continuousClock) var clock  // Controllable async sleep
  @Dependency(\.date.now) var now           // Controllable current date
  @Dependency(\.mainQueue) var mainQueue    // Controllable main queue scheduling
  @Dependency(\.uuid) var uuid              // Controllable UUID creation

  // ...
}

Как только ваши зависимости объявлены, вместо того, чтобы напрямую обращаться к Date(), UUID(), Task и т. д., вы можете использовать зависимость, определенную в модели вашей функции:

final class FeatureModel: ObservableObject {
  // ...

  func addButtonTapped() async throws {
    try await self.clock.sleep(for: .seconds(1))  // ???? Don't use 'Task.sleep'
    self.items.append(
      Item(
        id: self.uuid(),  // ???? Don't use 'UUID()'
        name: "",
        createdAt: self.now  // ???? Don't use 'Date()'
      )
    )
  }
}

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

Например, вы можете легко управлять этими зависимостями в тестах. Если вы хотите протестировать логику внутри метода addButtonTapped, вы можете использовать функцию withDependencies, чтобы переопределить любые зависимости в рамках одного теста. Это так же просто, как 1-2-3:

func testAdd() async throws {
  let model = withDependencies {
    // 1️⃣ Override any dependencies that your feature uses.
    $0.clock = ImmediateClock()
    $0.date.now = Date(timeIntervalSinceReferenceDate: 1234567890)
    $0.uuid = .incrementing
  } operation: {
    // 2️⃣ Construct the feature's model
    FeatureModel()
  }

  // 3️⃣ The model now executes in a controlled environment of dependencies,
  //    and so we can make assertions against its behavior.
  try await model.addButtonTapped()
  XCTAssertEqual(
    model.items,
    [
      Item(
        id: UUID(uuidString: "00000000-0000-0000-0000-000000000000")!,
        name: "",
        createdAt: Date(timeIntervalSinceReferenceDate: 1234567890)
      )
    ]
  )
}

Здесь мы управляли зависимостью date, чтобы всегда возвращать одну и ту же дату; и зависимостью uuid, чтобы возвращать автоматически увеличивающийся UUID при каждом его вызове. Если бы мы не контролировали эти зависимости, этот тест было бы очень сложно написать, поскольку невозможно точно предсказать, что вернут Date() и UUID().

Но управляемые зависимости полезны не только для тестов. Их также можно использовать в превью Xcode. Предположим, что приведенная выше функция использует часы, чтобы на определенное время уходить в режим ожидания, перед тем, как что-либо произойдет во вью. Если вы не хотите буквально ждать, пока пройдет время, чтобы увидеть, как изменится вью, вы можете переопределить зависимость часов, чтобы она была «немедленной» используя помощник withDependencies:

struct Feature_Previews: PreviewProvider {
  static var previews: some View {
    FeatureView(
      model: withDependencies {
        $0.clock = ImmediateClock()
      } operation: {
        FeatureModel()
      }
    )
  }
}

Таким образом превью использует “немедленные часы” при запуске, но при запуске в симуляторе или на устройстве он по-прежнему будет использовать действующие ContinuousClock. Это позволяет переопределять зависимости только для превью, не влияя на работу приложения в продакшене.

Это основы, чтобы начать пользоваться библиотекой, но вы можете сделать гораздо больше. Более подробно вы можете узнать в нашей документации и статьях; также мы пересобрали Scrumdinger от Apple, чтобы продемонстрировать, как с помощью этой библиотеки создать большое приложение с большим количеством зависимостей. Его можно найти здесь, в нашей библиотеке SwiftUINavigation.

Мультиплатформенный

Хотя эта библиотека зависимостей отлично работает для приложений на SwiftUI, она полезна и во многих других ситуациях.

UIKit

Её можно использовать в приложениях на UIKit точно так же, как и в приложениях на SwiftUI, за исключением того, что вместо добавления зависимостей в ObservableObject вы можете добавить их в ваши подклассы UIViewController:

class FeatureController: UIViewController {
  @Dependency(\.continuousClock) var clock
  @Dependency(\.date) var date
  @Dependency(\.mainQueue) var mainQueue
  @Dependency(\.uuid) var uuid

  // ...
}

Что позволяет создавать этот класс в контролируемой среде, например, в тестах и предварительных просмотрах Xcode.

Сторонние фреймворки

Сторонние фреймворки могут интегрировать библиотеку, чтобы предоставить своим пользователям систему зависимостей. Например, эта библиотека зависимостей “прокачивает” систему управления зависимостями для Компонуемой Архитектуры. На самом деле эта библиотека возникла из Компонуемой Архитектуры, но вскоре мы поняли, что она будет полезна в ванильном SwiftUI и других фреймворках, поэтому мы решили выделить ее в отдельную библиотеку (и мы делаем это уже в восьмой раз!) .

Серверный Swift

Её можно использовать даже с серверными приложениями. Кстати, наш сайт построен на Swift и теперь использует библиотеку зависимостей для управления зависимостями и делает наш серверный код более тестируемым.

И это лишь вершина айсберга.

Документация

Мы написали большое количество документации для этой библиотеки, в том числе подборку статей для изучения зависимостей, как их создавать, как их использовать в контекстах работы, теста и превью, и многое другое:

Начало

  • Быстрый старт: Изучите основы работы с библиотекой, прежде чем углубляться во все ее детали.

  • Что такое зависимости? Узнайте, что такое зависимости, как они усложняют ваш код и почему вы хотите ими управлять.

Необходимое

Продвинутый

  • Проектирование зависимостей: Изучите методы проектирования ваших зависимостей, чтобы они были наиболее гибкими для внедрения в функции и переопределения для тестов.

  • Переопределение зависимостей: Узнайте, как можно изменить зависимости во время выполнения, чтобы определенные части вашего приложения могли использовать разные зависимости.

  • Сроки жизни зависимостей: узнайте о сроках жизни зависимостей, о том, как продлить срок жизни зависимости и о том, как наследуются зависимости.

  • Системы с единой точкой входа: узнайте о системах с «единой точкой входа» и о том, почему они лучше всего подходят для этой библиотеки зависимостей, не смотря на то, что эту библиотеку можно использовать с другими системами.

Разное

Поддержка параллелизма: узнайте об инструментах параллелизма, поставляемых с библиотекой, которые упрощают написание тестов и реализацию зависимостей.

Начните работать с зависимостями сегодня!

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

Добавьте Зависимости 0.1.0 в свой проект сегодня, чтобы начать познавать эти концепции!

Комментарии (1)


  1. storoj
    20.01.2023 02:23

    Я чёт не понял, что изменится если убрать эти новые "слова" типа @Dependency и остального? Выглядит как какой-то сраный объект с полями, которые заполняются не в конструкторе, а в какой-то "withDependency". Зачем? Если можно точно так же, стандартными средствами языка вставить все те же самые зависимости через конструктор.