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

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

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

c8ec3211fce641f395423dfab42c92a3.jpgВведение В рамках этого цикла статей я не буду углубляться в теорию, рассматривать Dependency Inversion Principle или паттерны Dependency Injection — примем за данность, что читатель уже достаточно подготовлен к тому, чтобы познать дзен, и перейдем сразу к практике (ссылки для знакомства с теорией даны в самом конце поста).Typhoon Framework — это самая известная и популярная реализация DI-контейнера для Objective-C и Swift приложений. Проект достаточно молодой — первый коммит был сделан в самом конце 2012 года, но уже обзавелся большим количеством поклонников. Отдельного упоминания заслуживает активная поддержка проекта его создателями (один из которых, между прочим, живет и работает в Омске) — на большинство создаваемых Issue отвечают в течение десяти минут, а уже через несколько часов к обсуждению присоединяется вся команда.Зачем же нам нужен Typhoon? Отвечу одной аббревиатурой — IoC. Я уже пообещал не вдаваться в теорию, поэтому просто сошлюсь на Мартина Фаулера.

54f27bd5862e4caaaae682307849a97d.png

Посмотрим на несколько основных плюшек 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 startUpConfigurator;

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

— (RIAppDelegate *)appDelegate;

@end На самом деле, этот метод можно и не объявлять в интерфейсе —, но в образовательных целях оставим его здесь. Реализуем имплементацию RIAssembly: @implementation RIAssembly

— (RIAppDelegate *)appDelegate { return [TyphoonDefinition withClass:[RIAppDelegate class] configuration:^(TyphoonDefinition *definition) { [definition injectProperty:@selector (startUpConfigurator) with:[self startUpConfigurator]]; } }

— (id )startUpConfigurator { return [TyphoonDefinition withClass:[RIStartUpConfiguratorBase class]]; }

@end Заметьте, что в методе, возвращающем TyphoonDefinition для конфигуратора, никаких дополнительных инъекций не производится —, но сделать это в будущем ничто не мешает. К примеру, мы можем передать ему keyWindow приложения, чтобы появилась возможность выставить rootViewController. DI-контейнеры по своему определению должны быть максимально автоматизированы, мы не хотим вручную запрашивать что-то у TyphoonAssembly. Оптимальный вариант — использование Info.plist файла. Все, что от нас требуется — добавить под определенным ключом названия классов фабрик, которые должны активироваться при старте приложения.eb0c724405f34b0f9f88b48c7f11958a.png

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

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

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

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

Примеры использования Typhoon в Рамблер.Почте Для создания простого инстанса, реализующего определенный протокол, нужно лишь указать класс создаваемого объекта.  — (id )storyboardBuilder { return [TyphoonDefinition withClass:[RCMStoryboardBuilderBase class]]; } Для работы RCMAuthorizationPopoverBuilderBase требуется объект storyboardBuilder, создавать который мы уже научились. Для инъекции его в граф зависимостей нам всего лишь нужно вызвать соответствующий метод — [self storyboardBuilder]. Таким образом мы не только создали инстанс класса, но и установили все его зависимости.  — (id )authorizationPopoverBuilder { return [TyphoonDefinition withClass:[RCMAuthorizationPopoverBuilderBase class] configuration:^(TyphoonDefinition *definition) { [definition injectProperty:@selector (storyboardBuilder) with:[self storyboardBuilder]]; }]; } Мы хотим, чтобы объект класса RCMNetworkLoggerBase был синглтоном — в этом нам помогает свойство scope у TyphoonDefinition, отвечающее за настройку жизненного цикла объекта.  — (id )networkLogger { return [TyphoonDefinition withClass:[RCMNetworkLoggerBase class] configuration:^(TyphoonDefinition *definition) { definition.scope = TyphoonScopeSingleton; }]; } Посмотрим, как в Typhoon реализован Initializer Injection. Для работы сервису настроек требуются две обязательные зависимости — networkClient, умеющий работать с сетевыми запросами, и credentialsStorage, хранящий в себе различные учетные данные пользователя. Метод -useInitializer у TyphoonDefinition принимает селектор определенного init’а и блок, в котором в инициализатор внедряются его параметры.  — (id )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]]; }]; }]; } Теперь изучим реализацию Method Injection. Сервис ошибок умеет рассылать полученный NSError всем подписанным обработчикам. Чтобы все необработанные ошибки записывались в лог, мы хотим сразу после создания сервиса подписать стандартного обработчика.  — (id )errorService { return [TyphoonDefinition withClass:[RCMErrorServiceBase class] configuration:^(TyphoonDefinition *definition) { [definition injectMethod:@selector (addErrorHandler:) parameters:^(TyphoonMethod *method) { [method injectParameterWith:[self defaultErrorHandler]]; }]; }]; } У всех 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’ов. Вместо того, чтобы хардкодить URL’ы нашего API, мы храним их в конфигурационном plist-файле. В данной ситуации Typhoon помогает следующим — он сам подгружает требуемый файл и преобразует его поля в нативные объекты (в данном случае — NSURL), предоставляя нам удобный синтаксис для обращения к полям конфига — TyphoonConfig (KEY).  — (id)configurer { return [TyphoonDefinition configDefinitionWithName: RCMConfigFileName]; }

— (id)idXMLRPCClient{ return [TyphoonDefinition withClass:[RCMRPCClientBase class] configuration:^(TyphoonDefinition *definition) { [definition useInitializer:@selector (initWithBaseURL:) parameters:^(TyphoonMethod *initializer) { [initializer injectParameterWith: TyphoonConfig (RCMAuthorizationURLKey)]; }]; }]; } Мифы Среди тех, кто интересовался Typhoon Framework лишь поверхностно, бытуют несколько мифов, не имеющих под собой практически никаких оснований.Высокий порог вхожденияНа самом деле за несколько часов копания в исходниках, документации и семпловых проектах (которых, к слову, уже целых три) можно понять базовые принципы работы Typhoon и начать его использование в своем проекте. Если же нет желания сильно углубляться — можно вообще обойтись примерами и, не откладывая, интегрировать фреймворк в свой код. Сильное влияние на дебаггингФактических точек соприкосновения нашего кода с Typhoon на самом деле не так и много, и на этих стыках разработчиками уже предусмотрены информативные Exception’ы. Если Typhoon перестанут поддерживать, из проекта его не выпилитьTyphoon хорош тем, что мы с ним практически нигде не взаимодействуем напрямую — поэтому отказаться от него будет хоть и сложно, но все-таки возможно — достаточно лишь написать свой уровень фабрик и механизмы интегрирования его с кодом (пусть даже ручные). Но… там же свиззлинг! Objective-C знаменит своим рантаймом, и не использовать его возможности в таком фреймворке — как минимум глупо. К тому же, мы используем компонент как «черный ящик», полагаясь на то, что на все грабли уже успели наступить до нас те самые 1200 звездочек. Зачем мне Typhoon, когда я могу написать свой велосипед? Typhoon Framework серьезно уменьшает количество и сложность кода, отвечающего за создание графа объектов и передачу зависимостей при навигации между экранами. Кроме того, он дает нам централизованное управление зависимостями без недостатков самописного сервис-локатора. Простую фабрику, я думаю, может написать любой читатель, но, по мере развития проекта, вы будете продолжать сталкиваться с ограничениями этого подхода — и либо придется самим реализовывать то, что уже давно придумано и отлажено, либо жертвовать функциональностью и производительностью своего проекта. Заключение Первоначально я планировал полностью перевести свое выступление на Rambler.iOS к печатному виду — но, написав пару разделов, осознал, что для одной статьи материала получается слишком много. Поэтому, в следующих сериях: Внутреннее устройство Typhoon Framework и модульность фабрик Работа со storyboards, тестирование, autowire и другие плюшки Typhoon Framework. Полезные ссылки

© Habrahabr.ru