Pull to refresh

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

Reading time 8 min
Views 12K


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

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



Введение


Для начала разберем небольшой словарь терминов, которые будут активно использоваться в этой статье:
  • 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'ов, и затем на его основе либо создается новый инстанс, либо переиспользуется закэшированный.

Больше о процедуре активации можно узнать в следующих исходных файлах:
  • 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,
  • NSMutableOrderedSet *_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
    В процессе построения графа зависимостей объект с таким scope будет создан только один раз. Технически это выглядит так: перед созданием инстанса (какого-либо definition), создается пул для object graph scoped объектов, в процессе построения графа зависимостей, все object-graph-scoped уходят в этот пул, а после того, как инстанс создан, этот пул очищается.
  • 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 на модули и покрыть все это дело тестами.

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



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


Tags:
Hubs:
+10
Comments 0
Comments Leave a comment

Articles

Information

Website
rambler-co.ru
Registered
Employees
1,001–5,000 employees
Location
Россия