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

от автора

В прошлой части цикла мы познакомились с 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

Именно в засвиззленном 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 играет роль интерфейса для общения с настоящей фабрикой.

Посмотрим, что происходит, не особо вдаваясь в подробности:

  1. У TyphoonBlockComponentFactory вызывается инициализатор -initWithAssemblies:, на вход которому передается массив assembly, которые нужно активировать.
  2. Каждому из definition’ов, создаваемых активируемыми assembly, назначается свой уникальный ключ (рандомная строка + имя метода).
  3. Все TyphoonDefinition добавляются в массив registry только что созданного TyphoonBlockComponentFactory.

Конечно, этими тремя пунктами дело не ограничивается: для каждого зарегистрированного 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 использует свой сабкласс UIStoryboardTyphoonStoryboard. Первая особенность, бросающаяся в глаза — это фабричный метод, который отличается от своего родителя дополнительным параметром — factory:

+ (TyphoonStoryboard *)storyboardWithName:(NSString *)name                                 factory:(id<TyphoonComponentFactory>)factory                                 bundle:(NSBundle *)bundleOrNil; 

Именно в этой фабрике, реализующей протокол TyphoonComponentFactory, будет осуществляться поиск definition’ов для экранов текущей storyboard. Посмотрим на все этапы инжекции зависимостей во ViewController’ы:

  1. В первую очередь мы попадаем в метод -instantiateViewControllerWithIdentifier:, переопределенный в TyphoonStoryboard.
  2. Создается инстанс нужного контроллера путем вызова super’a.
  3. Инициируется инжекция всех зависимостей текущего контроллера и его дочерних контроллеров:
    — (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]; 	} }
  4. В 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:

  • TyphoonScopeObjectGraph
    Typhoon держит слабые ссылки на все зависимости такого объекта — таким образом, высвобождение объекта из памяти повлечет за собой очистку всего графа его зависимостей. Это особенно удобно при работе с 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»

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

ссылка на оригинал статьи http://habrahabr.ru/post/260355/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *