Dependency Injection в Objective-C с Магией и Кровью

от автора

Разделения на MVC недостаточно

С каждым днем iOS приложения становятся все более громоздкими, в следствие чего одного MVC становится мало.

Мы видим все больше и больше классов различного назначения: логика выносится в сервисы, модели оборачиваются декораторами, крупные представления разбиваются на более мелкие части. И самое главное, что в этом случае у нас появляется масса зависимостей, и мы должны ими как-то управлять.

Очень часто для решения проблемы зависимостей используется Singleton, по сути глобальная переменная, к которой все имеют доступ.
Как часто вам приходилось видеть подобный код?

[[RequestManager sharedInstance] loadResourcesAtPath:@"http://example.com/resources" withDelegate:self]; // или [[DatabaseManager sharedManager] saveResource:resource]; 

Этот подход используется во множестве проектов, но он имеет некоторые недостатки:

  • тяжело застабать синглтон который используется внутри тестируемого класса
  • синглтон, по сути, глобальная переменная
  • с точки зрения SRP объект не должен контролировать свое Singleton’овское поведение

Первую проблему решить довольно просто — нужно использовать свойства:

@interface ViewController : UIViewController  @property (nonatomic, strong) RequestManager *requestManager;  @end 

Но этот подход имеет другие минусы — теперь кто-то должен «заполнить» это свойство.
Магия Крови способствует решению этой проблемы.

Внедрение зависимостей

Эти проблемы не являются уникальными для Objective-C. Если мы посмотрим на более «промышленные» языки, такие как Java или C++, то сможем найти решение. Широко используемый подход в Java — Внедрение зависимостей (Dependency Injection, DI)

DI позволяет использовать requestManager в виде синглтона в приложении, но в тестах заменяя его на mock. При этом ни RequestManager ни ViewController не знает ничего о синглтонах, потому что это поведение контролирует DI фрэймворк.

На гитхабе лежит много реализаций DI на Objective-C, но они имеют свои минусы:

  • описание зависимостей с использованием макросов или строковых констант
  • внедрение происходит только если объект создан «особым» образом (этот вариант не будет работать с ViewController‘ами и View созданными из сторибордов и нибов)
  • внедряемый класс должен реализовать некий протокол (не будет работать со сторонними или стандартными библиотеками)
  • инициализацию невозможно вынести в отдельный модуль
  • XML

Магия Крови

Давайте посмотрим на очередной фрэймворк (с другими недостатками) — Магия Крови (BloodMagic, BM)

BM реализует подобие кастомных аттрибутов для свойств Objective-C классов. Он проектировался с учетом расширяемости и в скором времени будет добавлено больше фич. На данный момент реализован только один аттрибут — Lazy, Ленивая Инициализация.

Этот атрибут позволяет инициализировать свойства по требованию, без написания рутинного кода. Таким образом вместо подобных простыней:

@­interface ViewController : UIViewController  @­property (nonatomic, strong) ProgressViewService *progressViewService; @­property (nonatomic, strong) ResourceLoader *resourceLoader;  @­end  @­implementation ViewController  - (void)loadResources {     [self.progressViewService showProgressInView:self.view];          self.resourceLoader.delegate = self;     [self.resourceLoader loadResources]; }  - (ProgressViewService *)progressViewService {     if (_progressViewService == nil) {         _progressViewService = [ProgressViewService new];     }          return _progressViewService; }  - (ResourceLoader *)resourceLoader {     if (_resourceLoader == nil) {         _resourceLoader = [ResourceLoader new];     }          return _resourceLoader; }  @­end 

можно просто написать:

@­interface ViewController : UIViewController     <BMLazy>  @­property (nonatomic, strong) ProgressViewService *progressViewService; @­property (nonatomic, strong) ResourceLoader *resourceLoader;  @­end  @­implementation ViewController  @­dynamic progressViewService; @­dynamic resourceLoader;  - (void)loadResources {     [self.progressViewService showProgressInView:self.view];          self.resourceLoader.delegate = self;     [self.resourceLoader loadResources]; }  @­end 

И все. Оба @­dynamic свойства будут созданы при первом вызове self.progressViewService и self.resourceLoader. Эти объекты будут освобождены как и обыкновенные свойства — после освобождения ViewController‘а.

Магия Крови и Внедрение Зависимостей

По умолчанию для создания объектов используется метод класса +new. Но есть возможность описать и свои, кастомные инициализаторы, которые являются ключевой особенностью BM в качестве DI фрэймворка.

Создание кастомного инициализатора слегка многословное:

BMInitializer *initializer = [BMInitializer lazyInitializer]; initializer.propertyClass = [ProgressViewService class]; initializer.initializer = ^id (id sender){   return [[ProgressViewService alloc] initWithViewController:sender]; }; [initializer registerInitializer]; 

propertyClass — инициализатор регистрируется для свойств этого класса.
initializer — блок, который будет вызван для инициализации объекта. Если этот блок nil или инициализатор не найден, то объект будет создан при помощи метода +new.
sender — экземпляр класса контейнера.

Также инициализатор имеет свойство containerClass, которое позволяет описать создание одного и того же свойства по разному, основываясь на контейнере. К примеру:

BMInitializer *usersLoaderInitializer = [BMInitializer lazyInitializer]; usersLoaderInitializer.propertyClass = [ResourceLoader class]; usersLoaderInitializer.containerClass = [UsersViewController class]; usersLoaderInitializer.initializer = ^id (id sender){   return [ResourceLoader usersLoader]; }; [usersLoaderInitializer registerInitializer];  BMInitializer *projectsLoaderInitializer = [BMInitializer lazyInitializer]; projectsLoaderInitializer.propertyClass = [ResourceLoader class]; projectsLoaderInitializer.containerClass = [ProjectsViewController class]; projectsLoaderInitializer.initializer = ^id (id sender){   return [ResourceLoader projectsLoader]; }; [projectsLoaderInitializer registerInitializer]; 

Таким образом, для UsersViewController и ProjectsViewController будут созданы разные объекты. По умолчанию containerClass равен классу NSObject.

Инициализаторы помогают избавиться от различных shared* методов и хардкода, описанного в начале статьи:

BMInitializer *initializer = [BMInitializer lazyInitializer]; initializer.propertyClass = [RequestManager class]; initializer.initializer = ^id (id sender){    static id singleInstance = nil;   static dispatch_once_t once;   dispatch_once(&once, ^{     singleInstance = [RequestManager new];   });   return singleInstance;  }; [initializer registerInitializer]; 

Организация и хранение инициализаторов

В проекте может быть множество инициализаторов, потому имеет смысл вынести их в отдельное место/модуль.

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

Пример:

//  LoaderInitializer.m  #import <BloodMagic/Lazy.h>  #import "ResourceLoader.h" #import "UsersViewController.h" #import "ProjectsViewController.h"  lazy_initializer ResourseLoaderInitializers() {     BMInitializer *usersLoaderInitializer = [BMInitializer lazyInitializer];     usersLoaderInitializer.propertyClass = [ResourceLoader class];     usersLoaderInitializer.containerClass = [UsersViewController class];     usersLoaderInitializer.initializer = ^id (id sender){         return [ResourceLoader usersLoader];     };     [usersLoaderInitializer registerInitializer];          BMInitializer *projectsLoaderInitializer = [BMInitializer lazyInitializer];     projectsLoaderInitializer.propertyClass = [ResourceLoader class];     projectsLoaderInitializer.containerClass = [ProjectsViewController class];     projectsLoaderInitializer.initializer = ^id (id sender){         return [ResourceLoader projectsLoader];     };     [projectsLoaderInitializer registerInitializer]; } 

lazy_initializer будет заменен на __attribute__((constructor)) static void. Атрибут constructor означает что этот метод будет вызван раньше чем main (здесь есть более детальное описание: GCC. Function Attributes).

Планы на ближайшее будущее

  • реализовать поддержку протоколов (@­property (nonatomic, strong) id<ResourceLoader> loader)
  • добавить описание работы и реализации
  • описать добавление новых атрибутов
  • добавить больше атрибутов

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


Комментарии

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

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