Управляємо залежностями в 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 використовує свій сабкласс UIStoryboard TyphoonStoryboard. Перша особливість, що кидається в очі — це фабричний метод, який відрізняється від свого батька додатковим параметром — 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»

Корисні посилання


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.