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

в 20:46, , рубрики: bloodmagic, dependency injection, магия крови, Проектирование и рефакторинг, разработка под iOS, тяжелые, убей синглтон

Разделения на 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)
  • добавить описание работы и реализации
  • описать добавление новых атрибутов
  • добавить больше атрибутов

Автор: 1101_debian

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js