company_banner

Управляем зависимостями в iOS-приложениях правильно: Знакомство с Typhoon

    «Any magic, sufficiently analyzed is indistinguishable from technology.»

    Артур Кларк
    (эпиграф в официальной wiki проекта Typhoon Framework)




    Цикл «Управляем зависимостями в iOS-приложениях правильно»



    Введение


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

    Typhoon Framework — это самая известная и популярная реализация DI-контейнера для Objective-C и Swift приложений. Проект достаточно молодой — первый коммит был сделан в самом конце 2012 года, но уже обзавелся большим количеством поклонников. Отдельного упоминания заслуживает активная поддержка проекта его создателями (один из которых, между прочим, живет и работает в Омске) — на большинство создаваемых Issue отвечают в течение десяти минут, а уже через несколько часов к обсуждению присоединяется вся команда.

    Зачем же нам нужен Typhoon? Отвечу одной аббревиатурой — IoC. Я уже пообещал не вдаваться в теорию, поэтому просто сошлюсь на Мартина Фаулера.



    Посмотрим на несколько основных плюшек Typhoon'а, которых должно быть достаточно, чтобы привлечь внимание любого iOS-разработчика:
    • Абсолютно и полностью нативен. Никаких XML, макросов или магических строк — полностью поддерживаются все те немногочисленные плюшки, которые нам предоставляет Xcode: рефакторинг, автодополнение, проверка кода на этапе компиляции.
    • Превосходно реализована модульность — можно работать с любым количеством фабрик, разбитых как на вертикальные, так и на горизонтальные слои.
    • Полностью интегрирован со Storyboard'ами, позволяет внедрять любые зависимости прямиком во ViewController'ы.
    • Поддерживает все паттерны Dependency Injection: Initializer Injection, Property Injection и Method Injection (а для последнего еще и предусмотрены специальные хуки).
    • Поддерживает инъекцию circular dependencies — как пример, ViewController держит объект и является его делегатом.
    • Борцы с большими бинарниками могут спать спокойно — все модули фреймворка занимают всего лишь 3000 строк кода (как два обычных ViewController'a).


    Базовая интеграция с проектом


    Чтобы показать, насколько просто встраивается Typhoon в чистое приложение, рассмотрим кейс, в котором мы хотим внедрить в AppDelegate объект startUpConfigurator, умеющий правильным образом конфигурировать наше приложение.

    @interface RIAppDelegate
    
    @property (strong, nonatomic) id <RIStartUpConfigurator> startUpConfigurator;
    
    @end
    

    1. Создаем свой сабкласс TyphoonAssembly (который и является нашим DI-контейнером):

      @interface RIAssembly : TyphoonAssembly
      
      - (RIAppDelegate *)appDelegate;
      
      @end
      

      На самом деле, этот метод можно и не объявлять в интерфейсе — но в образовательных целях оставим его здесь.

    2. Реализуем имплементацию RIAssembly:

      @implementation RIAssembly
      
      - (RIAppDelegate *)appDelegate {
          return [TyphoonDefinition withClass:[RIAppDelegate class] configuration:^(TyphoonDefinition *definition) {
                  [definition injectProperty:@selector(startUpConfigurator) with:[self startUpConfigurator]];
          }
      }
      
      - (id <RIStartUpConfigurator>)startUpConfigurator {
          return [TyphoonDefinition withClass:[RIStartUpConfiguratorBase class]];
      }
      
      @end
      

      Заметьте, что в методе, возвращающем TyphoonDefinition для конфигуратора, никаких дополнительных инъекций не производится — но сделать это в будущем ничто не мешает. К примеру, мы можем передать ему keyWindow приложения, чтобы появилась возможность выставить rootViewController.

    3. DI-контейнеры по своему определению должны быть максимально автоматизированы, мы не хотим вручную запрашивать что-то у TyphoonAssembly. Оптимальный вариант — использование Info.plist файла. Все, что от нас требуется — добавить под определенным ключом названия классов фабрик, которые должны активироваться при старте приложения.



      На этом вся конфигурация закончена. Посмотрим, что в итоге у нас получилось.

    4. Ставим брейкпойнт в методе -applicationDidFinishLaunching и запускаем приложение:



      Конфигуратор успешно проинжектился с нужным нам классом (напоминаю, что сам RIAppDelegate будет работать с ним по определенному нами протоколу).

    Как видите, базовая интеграция Typhoon занимает всего пару минут. Более того, встроить фреймворк можно и в уже давно написанное приложение — но в этом случае степень удовольствия будет зависеть от качества дизайна кода.

    Но кому интересны инструкции по настройке — давайте лучше посмотрим на реальные кейсы использования Typhoon в проекте Рамблер.Почта.

    Примеры использования Typhoon в Рамблер.Почте


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

      - (id <RCMStoryboardBuilder>)storyboardBuilder {
          return [TyphoonDefinition withClass:[RCMStoryboardBuilderBase class]];
      }
      

    2. Для работы RCMAuthorizationPopoverBuilderBase требуется объект storyboardBuilder, создавать который мы уже научились. Для инъекции его в граф зависимостей нам всего лишь нужно вызвать соответствующий метод — [self storyboardBuilder]. Таким образом мы не только создали инстанс класса, но и установили все его зависимости.

      - (id <RCMPopoverBuilder>)authorizationPopoverBuilder {
            return [TyphoonDefinition withClass:[RCMAuthorizationPopoverBuilderBase class] 
                                       configuration:^(TyphoonDefinition *definition) {
                  [definition injectProperty:@selector(storyboardBuilder)
                                                         with:[self storyboardBuilder]];
          }];
      }
      

    3. Мы хотим, чтобы объект класса RCMNetworkLoggerBase был синглтоном — в этом нам помогает свойство scope у TyphoonDefinition, отвечающее за настройку жизненного цикла объекта.

      - (id <RCMLogger>)networkLogger {
            return [TyphoonDefinition withClass:[RCMNetworkLoggerBase class] 
                                       configuration:^(TyphoonDefinition *definition) {
                  definition.scope = TyphoonScopeSingleton;
          }];
      }
      

    4. Посмотрим, как в Typhoon реализован Initializer Injection. Для работы сервису настроек требуются две обязательные зависимости — networkClient, умеющий работать с сетевыми запросами, и credentialsStorage, хранящий в себе различные учетные данные пользователя. Метод -useInitializer у TyphoonDefinition принимает селектор определенного init'а и блок, в котором в инициализатор внедряются его параметры.

      - (id <RCMSettingsService>)settingsService {
              return [TyphoonDefinition withClass:[RCMSettingsServiceBase class]
                                                      configuration:^(TyphoonDefinition *definition) {
                                    [definition useInitializer:@selector(initWithClient:sessionStorage:)
                                                           parameters:^(TyphoonMethod *initializer) {
                                                        [initializer injectParameterWith:[self mailXMLRPCClient]];
                                                        [initializer injectParameterWith:[self credentialsStorage]];
                                     }];
          }];
      }
      

    5. Теперь изучим реализацию Method Injection. Сервис ошибок умеет рассылать полученный NSError всем подписанным обработчикам. Чтобы все необработанные ошибки записывались в лог, мы хотим сразу после создания сервиса подписать стандартного обработчика.

      - (id <RCMErrorService>)errorService {
              return [TyphoonDefinition withClass:[RCMErrorServiceBase class]
                                                        configuration:^(TyphoonDefinition *definition) {
                  [definition injectMethod:@selector(addErrorHandler:)
                                         parameters:^(TyphoonMethod *method) {
                                    [method injectParameterWith:[self defaultErrorHandler]];
                  }];
          }];
      }
      

    6. У всех ViewController'ов приложения обязательно должны быть три зависимости — сервис обработки ошибок, которому прокидываются все полученные объекты NSError, базовый обработчик ошибок, умеющий должным образом обрабатывать некие общие коды ошибок, и базовый роутер. Чтобы избежать дублирования этого кода для всех контроллеров, инъекцию этих трех зависимостей мы вынесли в базовый TyphoonDefinition. Для использования его в других методах достаточно лишь установить свойство parent.

      - (UIViewController *)baseViewController {
          return [TyphoonDefinition withClass:[UIViewController class] 
                                               configuration:^(TyphoonDefinition *definition) {
                  [definition injectProperty:@selector(errorService)
                                                       with:[self.serviceComponents errorService]];
                  [definition injectProperty:@selector(errorHandler)
                                                       with:[self baseControllerErrorHandler]];
                  [definition injectProperty:@selector(router)
                                                       with:[self baseRouter]];
          }];
      }
      
      - (UIViewController *)userNameTableViewController {
          return [TyphoonDefinition withClass:[RCMMessageCompositionViewController class] 
                                                    configuration:^(TyphoonDefinition *definition) {
                  definition.parent = [self baseViewController];
                  [definition injectProperty:@selector(router)
                                                        with:[self settingsRouter]];
          }];
      }
      

      Стоит отметить, что Typhoon позволяет строить и гораздо более сложные цепочки наследования TyphoonDefinition'ов.

    7. Вместо того, чтобы хардкодить URL'ы нашего API, мы храним их в конфигурационном plist-файле. В данной ситуации Typhoon помогает следующим — он сам подгружает требуемый файл и преобразует его поля в нативные объекты (в данном случае — NSURL), предоставляя нам удобный синтаксис для обращения к полям конфига — TyphoonConfig(KEY).

      - (id)configurer {
          return [TyphoonDefinition configDefinitionWithName:RCMConfigFileName];
      }
      
      - (id<RCMRPCClient>)idXMLRPCClient{
          return [TyphoonDefinition withClass:[RCMRPCClientBase class] configuration:^(TyphoonDefinition *definition) {
              [definition useInitializer:@selector(initWithBaseURL:) parameters:^(TyphoonMethod *initializer) {
                  [initializer injectParameterWith:TyphoonConfig(RCMAuthorizationURLKey)];
              }];
          }];
      }
      


    Мифы


    Среди тех, кто интересовался Typhoon Framework лишь поверхностно, бытуют несколько мифов, не имеющих под собой практически никаких оснований.

    1. Высокий порог вхождения
      На самом деле за несколько часов копания в исходниках, документации и семпловых проектах (которых, к слову, уже целых три) можно понять базовые принципы работы Typhoon и начать его использование в своем проекте. Если же нет желания сильно углубляться — можно вообще обойтись примерами и, не откладывая, интегрировать фреймворк в свой код.

    2. Сильное влияние на дебаггинг
      Фактических точек соприкосновения нашего кода с Typhoon на самом деле не так и много, и на этих стыках разработчиками уже предусмотрены информативные Exception'ы.

    3. Если Typhoon перестанут поддерживать, из проекта его не выпилить
      Typhoon хорош тем, что мы с ним практически нигде не взаимодействуем напрямую — поэтому отказаться от него будет хоть и сложно, но все-таки возможно — достаточно лишь написать свой уровень фабрик и механизмы интегрирования его с кодом (пусть даже ручные).

    4. Но… там же свиззлинг!
      Objective-C знаменит своим рантаймом, и не использовать его возможности в таком фреймворке — как минимум глупо. К тому же, мы используем компонент как «черный ящик», полагаясь на то, что на все грабли уже успели наступить до нас те самые 1200 звездочек.

    5. Зачем мне Typhoon, когда я могу написать свой велосипед?
      Typhoon Framework серьезно уменьшает количество и сложность кода, отвечающего за создание графа объектов и передачу зависимостей при навигации между экранами. Кроме того, он дает нам централизованное управление зависимостями без недостатков самописного сервис-локатора. Простую фабрику, я думаю, может написать любой читатель, но, по мере развития проекта, вы будете продолжать сталкиваться с ограничениями этого подхода — и либо придется самим реализовывать то, что уже давно придумано и отлажено, либо жертвовать функциональностью и производительностью своего проекта.

    Заключение


    Первоначально я планировал полностью перевести свое выступление на Rambler.iOS к печатному виду — но, написав пару разделов, осознал, что для одной статьи материала получается слишком много. Поэтому, в следующих сериях:

    Цикл «Управляем зависимостями в iOS-приложениях правильно»



    Полезные ссылки


    • +18
    • 27,5k
    • 6
    Rambler&Co 82,21
    Компания
    Поделиться публикацией
    Комментарии 6
    • 0
      Простите за возможно нубский вопрос — есть какая-нибудь связь этого фреймворка и cocoapods?
      • 0
        Есть, в podfile добавляем:
        pod 'Typhoon'
        

        Есть и поддержка Carthage:
        github "appsquickly/Typhoon"
        
        • +4
          Я думаю, Speakus спрашивает про связь/разницу между «зависимостями» в CocoaPods и в Typhoon. Если так — связи нет.

          В терминологии систем сборки и управления зависимостями типа CocoaPods, зависимость — это код/библиотека, которая нужна для сборки приложения, скажем Typhoon может быть зависимостью для вашего приложения и вы можете подключить его через CocoaPods.

          В терминологии Dependency Injection, зависимость — это то, что нужно классу/объекту для работы. Скажем, объекту TweetsViewController требуется для работы объект TwitterApi, тогда можно сказать, что TwitterApi — это dependency (зависимость) для TweetsViewController.

          Как то так.

          P.S. не iOS разработчик.
      • +1
        Совершенно замечательная статья, ждем продолжение.
        Давно поглядываю на этот самый Typhoon Framework, но никак не соберусь попробовать на практике.

        Кстати, кто знает, почему IoC-контейнеры в iOS настолько непопулярны?
        Судя по всему, Typhoon Framework это единственное зрелое решение на сегодняшний день, в то время как в других языках реализации IoC-контейнеров исчисляются десятками (или даже сотнями) и используются весьма широко.
        • +1
          Это проблема квалификации мобильных разработчиков в целом, скажем, для Android доступно большое количество Java DI фреймворков, но очень немногие команды/проекты их используют (как и тесты, CI и тд). Проблема не в iOS, имхо :)
          • +1
            Спасибо за отзыв :)
            По поводу DI фреймворков соглашусь с Artem_zin — не настолько они востребованы среди разработчиков. Тем не менее, помимо Typhoon можно глянуть на Objection и BloodMagic (от 1101_debian) — но, на мой взгляд, не конкуренты.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое