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

3d4caa431b6a42c4a8ce1872531f7154.pngВ прошлой части цикла мы познакомились с Dependency Injection фреймворком для iOS — Typhoon, и рассмотрели базовые примеры его использования в проекте Рамблер.Почта. В этот раз мы углубимся в изучение его внутреннего устройства.

ВведениеДля начала разберем небольшой словарь терминов, которые будут активно использоваться в этой статье: Assembly (читается как [эссэмбли]). Ближайший русский эквивалент — сборка, конструкция. В Typhoon — это объекты, содержащие в себе конфигурации всех зависимостей приложения, по сути своей являются костяком всей архитектуры. Для внешнего мира, будучи активированными, ведут себя как обычные фабрики. Definition. Что касается перевода на русский язык — мне больше всего импонирует конфигурация, как наиболее близкий к оригиналу вариант. TyphoonDefinition — это объекты, являющиеся своеобразной моделью зависимостей, содержат в себе такую информацию, как класс создаваемого объекта, его свойства, тип жизненного цикла. Большинство примеров из предыдущей статьи касались как раз таки различных вариантов настройки TyphoonDefinition. Scope. Здесь все просто — это тип жизненного цикла объекта, созданного при помощи Typhoon. Активация. Процесс, в результате которого все объекты-наследники TyphoonAssemby начинают вместо TyphoonDefinition отдавать реальные инстансы классов. Суть и принцип работы активации рассмотрим чуть ниже. И еще раз делаю упор на том, что очень важно разобраться в базовых принципах работы фреймворка — после этого мы спокойно сможем двинуться дальше и изучить все прочие плюшки Typhoon, не останавливаясь на деталях их реализации.Чтобы не захламлять статью огромными листингами кода, я буду периодически ссылаться на определенные файлы фреймворка, а приводить лишь самые интересные моменты. Обращаю ваше внимание на то, что актуальная версия Typhoon Framework на момент написания статьи — 3.1.7.

Инициализация Жизненный цикл приложения с использованием Typhoon выглядит следующим образом: Вызов main.m Создание UIApplication — [UIApplication init] Создание UIAppDelegate — [UIAppDelegate init] Вызов метода setDelegate: у созданного инстанса UIApplication Вызов засвиззленной в классе TyphoonStartup имплементации setDelegate: Вызов метода -applicationDidFinishLaunching: withOptions: у инстанса UIAppDelegate 8633761a37ab4a928ede079504e738d5.pngИменно в засвиззленном setDelegate: и происходит создание и активация стартовых assemblies.

Автоматическая загрузка фабрик возможна в двух случаях: мы либо указали их классы в Info.plist под ключом TyphoonInitialAssemblies:

+ (id)factoryFromPlistInBundle:(NSBundle *)bundle + (id)factoryFromPlistInBundle:(NSBundle *)bundle { TyphoonComponentFactory *result = nil;

NSArray *assemblyNames = [self plistAssemblyNames: bundle]; NSAssert (! assemblyNames || [assemblyNames isKindOfClass:[NSArray class]], @«Value for 'TyphoonInitialAssemblies' key must be array»);

if ([assemblyNames count] > 0) { NSMutableArray *assemblies = [[NSMutableArray alloc] initWithCapacity:[assemblyNames count]]; for (NSString *assemblyName in assemblyNames) { Class cls = TyphoonClassFromString (assemblyName); if (! cls) { [NSException raise: NSInvalidArgumentException format:@«Can’t resolve assembly for name %@», assemblyName]; } [assemblies addObject:[cls assembly]]; } result = [TyphoonBlockComponentFactory factoryWithAssemblies: assemblies]; }

return result; } либо реализовали метод -initialFactory в нашем AppDelegate:+ (TyphoonComponentFactory *)factoryFromAppDelegate:(id)appDelegate + (TyphoonComponentFactory *)factoryFromAppDelegate:(id)appDelegate { TyphoonComponentFactory *result = nil;

if ([appDelegate respondsToSelector:@selector (initialFactory)]) { result = [appDelegate initialFactory]; }

return result; } Если не было сделано ни того, ни другого — assembly придется создавать руками в каком-либо другом месте кода, что делать не рекомендуется.Больше о деталях инициализации Typhoon можно узнать в следующих исходных файлах:

TyphoonStartup.m TyphoonComponentFactory.m Активация Этот процесс является ключевым в работе фреймворка. Под активацией понимается создание объекта класса TyphoonBlockComponentFactory, инстанс которого находится «под капотом» у всех активированных assembly. Таким образом, любая assembly играет роль интерфейса для общения с настоящей фабрикой.Посмотрим, что происходит, не особо вдаваясь в подробности:

У TyphoonBlockComponentFactory вызывается инициализатор -initWithAssemblies:, на вход которому передается массив assembly, которые нужно активировать. Каждому из definition’ов, создаваемых активируемыми assembly, назначается свой уникальный ключ (рандомная строка + имя метода). Все TyphoonDefinition добавляются в массив registry только что созданного TyphoonBlockComponentFactory. 091d7f47c3424315a643dbe8569bb329.pngКонечно, этими тремя пунктами дело не ограничивается: для каждого зарегистрированного TyphoonDefinition добавляются аспекты, геттеры всех зависимостей свиззлятся, создавая тем самым цепочку инициализации графа объектов, в TyphoonBlockComponentFactory создаются пулы инстансов — в общем и целом, для обеспечения работы фреймворка производится большое количество различных действий. В рамках этой статьи мы не будем вдаваться в подробности каждой из рассматриваемых процедур, так как это может отвлечь от понимания общих принципов работы Typhoon.

Мы рассмотрели, как TyphoonAssembly активируется — осталось понять, зачем это вообще нужно делать. Каждый раз, когда мы вручную дергаем у assembly какой-нибудь метод, отдающий TyphoonDefinition для зависимости, происходит следующее:

— (void)forwardInvocation:(NSInvocation *)anInvocation  — (void)forwardInvocation:(NSInvocation *)anInvocation { if (_factory) { [_factory forwardInvocation: anInvocation]; } … } В _factory полученный NSInvocation обрабатывается и преобразуется в вызов следующего метода:- (id)componentForKey:(NSString *)key args:(TyphoonRuntimeArguments *)args  — (id)componentForKey:(NSString *)key args:(TyphoonRuntimeArguments *)args { if (! key) { return nil; }

[self loadIfNeeded];

TyphoonDefinition *definition = [self definitionForKey: key]; if (! definition) { [NSException raise: NSInvalidArgumentException format:@«No component matching id '%@'.», key]; }

return [self newOrScopeCachedInstanceForDefinition: definition args: args]; } По сгенерированному из selector’а метода ключу достается один из зарегистрированных в TyphoonBlockComponentFactory definition’ов, и затем на его основе либо создается новый инстанс, либо переиспользуется закэшированный.На самом деле, более чем вероятно, что вручную обращаться к assembly вам не придется (как никак, inversion of control) — поэтому плавно перейдем к рассмотрению механизмов работы со storyboard.

Больше о процедуре активации можно узнать в следующих исходных файлах:

TyphoonAssembly.m TyphoonBlockComponentFactory.m TyphoonTypeDescriptor.m TyphoonAssemblyDefinitionBuilder.m TyphoonStackElement.m Работа со Storyboard Под капотом Typhoon использует свой сабкласс UIStoryboard — TyphoonStoryboard. Первая особенность, бросающаяся в глаза — это фабричный метод, который отличается от своего родителя дополнительным параметром — factory: + (TyphoonStoryboard *)storyboardWithName:(NSString *)name factory:(id)factory bundle:(NSBundle *)bundleOrNil; Именно в этой фабрике, реализующей протокол TyphoonComponentFactory, будет осуществляться поиск definition’ов для экранов текущей storyboard. Посмотрим на все этапы инжекции зависимостей во ViewController’ы: В первую очередь мы попадаем в метод -instantiateViewControllerWithIdentifier:, переопределенный в TyphoonStoryboard. Создается инстанс нужного контроллера путем вызова super’a. Инициируется инжекция всех зависимостей текущего контроллера и его дочерних контроллеров:- (void)injectPropertiesForViewController:(UIViewController *)viewController  — (void)injectPropertiesForViewController:(UIViewController *)viewController { if (viewController.typhoonKey.length > 0) { [self.factory inject: viewController withSelector: NSSelectorFromString (viewController.typhoonKey)]; } else { [self.factory inject: viewController]; }

for (UIViewController *controller in viewController.childViewControllers) { [self injectPropertiesForViewController: controller]; } } В TyphoonBlockComponentFactory происходит уже знакомая нам процедура — ищется соответствующий текущему классу TyphoonDefinition и инициируется процесс инжекции в нее графа зависимостей. Сейчас я не буду останавливаться на конкретной реализации работы с TyphoonStoryboard в приложении — эта тема будет затронута в одной из следующих статей.Подробнее о реализации работы со storyboard можно узнать в следующих исходных файлах:

TyphoonStoryboard.m TyphoonBlockComponentFactory.m TyphoonDefinition Практически в каждом приведенном мною сниппете в том или ином виде встречается класс TyphoonDefinition. Как я уже упоминал при перечислении терминов, TyphoonDefinition — это своего рода конфигурационный класс для создаваемой зависимости — поэтому для нас в первую очередь представляет интерес именно его интерфейс: Class _type — класс создаваемой зависимости NSString *_key — уникальный ключ, генерируемый при активации Typhoon, TyphoonMethod *_initializer — объект, создаваемый при initializer injection, содержащий в себе сигнатуру нужного инициализатора и коллекцию его параметров, TyphoonMethod *_beforeInjections — метод, который будет вызван до проведения инъекции зависимостей, TyphoonMethod *_afterInjections — метод, который будет вызван после проведения инъекции зависимостей, NSMutableSet *_injectedProperties — коллекция зависимостей, устанавливаемых через property injection, NSMutableSet *_injectedMethods — коллекция методов, в которые передаются определенные зависимости (method injection), TyphoonScope scope — тип жизненного цикла создаваемого объекта, TyphoonDefinition *_parent — базовый TyphoonDefinition, все свойства которой будут унаследованы текущей, BOOL abstract — флаг, указывающий на то, что текущая конфигурация может использоваться только для реализации наследования, и представляемый ею объект никогда не должен создаваться напрямую. Из всех вышеперечисленных свойств отдельного внимания заслуживает scope объекта.Подробнее о принципах работы TyphoonDefinition можно узнать в следующих исходных файлах:

TyphoonDefinition.m TyphoonAssemblyDefinitionBuilder.m TyphoonFactoryDefinition.m TyphoonInjectionByReference.m TyphoonMethod.m TyphoonScope Важно четко понимать, что, говоря о разных типах жизненного цикла объекта, мы все равно жестко привязаны к lifetime используемого инстанса TyphoonBlockComponentFactory — если эта фабрика будет высвобождена из памяти, вместе с ней освободятся и все графы объектов.Посмотрим, к чему приводит каждое из значений TyphoonScope: TyphoonScopeObjectGraphTyphoon держит слабые ссылки на все зависимости такого объекта — таким образом, высвобождение объекта из памяти повлечет за собой очистку всего графа его зависимостей. Это особенно удобно при работе с ViewController’ами — каждый раз при уходе с экрана автоматически очищаются все ставшие ненужными объекты. TyphoonScopePrototypeПри каждом обращении к TyphoonDefinition с таким scope будет создаваться новый инстанс класса. TyphoonScopeSingletonОбъект с таким жизненным циклом будет жить на всем протяжении жизни TyphoonComponentFactory. TyphoonScopeLazySingletonКак видно из названия — это синглтон, который будет создан в момент первого обращения к нему. TyphoonScopeWeakSingletonСинглтон, создаваемый при использовании такого TyphoonDefinition, находится в памяти ровно до тех пор, пока на него ссылается хотя бы один объект — в противном случае, он будет освобожден. Созданный объект, в зависимости от свойства scope его конфигурации, хранится в одном из пулов TyphoonComponentFactory, каждый из которых работает определенным образом.Больше о принципах работы кэша зависимостей Typhoon можно узнать в следующих исходниках:

TyphoonComponentFactory.m TyphoonWeakComponentPool.m TyphoonCallStack.m Заключение Мы успели рассмотреть только самые базовые принципы работы Typhoon Framework — инициализацию, активацию фабрик, устройство TyphoonAssembly, TyphoonStoryboard, TyphoonDefinition и TyphoonBlockComponentFactory, особенности жизненного цикла создаваемых объектов. Библиотека содержит в себе еще очень много интересных концепций, реализация которых порой просто завораживает.Я настоятельно рекомендую уделить несколько дней и зарыться в их изучение с головой — это более чем достойная альтернатива изучению многочисленных уроков в стиле «Работаем в Xcode мышкой на Swift бесплатно и без СМС» и «Продвинутая анимация индикатора загрузки файлов».

А в следующей серии вы узнаете, как избежать появления одной огромной фабрики, правильно разбить архитектуру уровня Assembly на модули и покрыть все это дело тестами.

Цикл «Dependency Injection в iOS» Полезные ссылки

© Habrahabr.ru