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

http://l.rw.rw/dibm
  • Перевод

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


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

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

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

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

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

  • синглтон, который используется внутри тестируемого класса, тяжело заменить на mock-объект
  • по сути синглтон это глобальная переменная
  • с точки зрения 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, bm_lazy) ProgressViewService *progressViewService;
@­property (nonatomic, strong, bm_lazy) 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 loader)
    добавить описание работы и реализации
    описать добавление новых атрибутов
    добавить больше атрибутов
Поделиться публикацией
Похожие публикации
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 23
  • –1
    тяжело застабать синглтон

    Извините, что сделать?

    Не раскрыто, почему это удобней, чем реализовать например у синглота метод sharedInstance, где с помощью dispatch_once будет по требованию создаваться один экземпляр. А для тестов(если для тестов нужна другая инициализация) либо сделать sharedInstanceTest, либо закрыть инициализацю с помощью директив препроцессора и котролировать это конфигурациями сборки (хотя не очень элегантный вариант).
    В общем не очень убедительно.
    • 0
      Я не нашел на что заменить слово «застабать».

      sharedInstanceTest, #ifndef'ы, еще можно категорию написать, которая перекроет метод или добавит метод setSharedInstance — это все, имхо, костыли.

      Удобство свойства в том, что всякий раз в тесте можно указать новый мок-объект, который будет делать то что нужно.
      • 0
        Если не находите нормального перевода, то лучше в криво переведенном слове в скобках укажите оригинал или непосредственно предложение, в котором оно используется в оригинале.
        Сам так делаю, когда про Core Data перевожу.
      • +1
        stub [stʌb] — заглушить

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

      Скорее всего не избавляет, а переносит их определения в другой конфигурационный файл. Программисту обычно легче работать видя полностью исходный код в файле, а не переключаясь между исходным кодом и конфигурационным файлом. Но, конечно, данный подход преимущество при создании моков дает.
      • 0
        Тут суть в том, что какой-нибудь ViewController не видит shared* метода, а просто пользуется своим свойством. Избавление от этих методов скорее влияет на код конечного пользователя (ViewController в данном случае), а не на код самого синглтона. Можно без проблем оставить метод sharedInstance в самом классе и возвращать его в блоке инициализации.
      • 0
        Я хотел сказать то, что в данном случае вместо того, чтобы перегрузить свойство в .m файле, нам надо определять его в конфигурационном файле. Случаи, когда объект создается просто с помощью [MyClass new] не так уж часты, как хотелось бы.
        • 0
          Не соглашусь, у меня есть масса сервисов которые именно так и создаются: ProgressViewService, ImageLoaderService, SomeResourceLoader и т.д.
        • 0
          Почти в каждом приложении есть некая сущность, экземпляр которой должен быть единым в системе, но это не значит, что эту сущность надо проектировать как синглтон.

          Мне кажется, это даже вредно, и надо противиться всякому «синглтоновому» ходу мысли. Сущность надо проектировать с расчётом на то, что её экземпляр будет одним из многих. Чтоб потом гипотетически как можно менее трудозатратно перетащить этот класс в другой проект.

          Уже позже, синглтон из сущности можно сделать двумя способами:
          1) В приложении уже есть один синглтон, от которого не уйдешь — апп делегат. Достаточно добавить туда рид-онли свойство, и создать категорию над сущностью с методом sharedInstance, который вытаскивает это свойство из апп делегата. Инициализация свойства в апп делегате будет ленива. (Этот способ подойдет, когда класс сущности потом не будет «библиотечным»)
          2) Если класс сущности библиотечный, удобнее всего метод sharedInstance делать через dispatch_once

          Таким образом синглтон — это просто дополнительная фича к классу, не более.

          Страшная история: один раз от индусов в руки попал проект, где каждый из, приблизительно, 30-ти UIViewController'ов был синглтоном.
          • 0
            При реализации синглтона через AppDelegate вылезет другая беда: повысится связность классов. То есть по сути, об этом объекте в программе будут знать все, кому не лень.
            • 0
              неа, #import «AppDelegate.h» будет только в файле Entity+App.m

              а вот куда втыкать #import «Entity+App.h» (где нужно или в .pch) — решать разработчику.
              • 0
                В моем случае этот вариант не очень подходит, т.к. я всячески против хранения каких-либо данных в AppDelegate, да и к категориям отношусь не очень хорошо.
                • 0
                  понимаю, есть доля здравого смысла, чтобы не хранить данные в апп делегате. Кроме того, завязывание на него выглядит не очень красиво, когда есть gcd.

                  а разверните, почему к категориям плохо относитесь?

                  Мне, например, по-кайфу писать код вроде:

                  pageURL = rawURLString.URL.URLWithoutAnchor.URLWithoutParameters;

                  return [url URLByModifyingParams:^(NSMutableDictionary *params) {
                  params[@«per_page»] = @«48»;
                  }];


                  components = [components map:^WAComponent *(WAComponent *oldComponent) {
                  return [oldComponent.assumptionID isEqual:component.assumptionID]
                  ? c.component
                  : oldComponent;
                  }];


                  — (BOOL)isTerminalSessionStatus {
                  return
                  [StoredSession.terminalSessionStatuses firstObjectWithPredicate:^BOOL(NSNumber *s) {
                  return [s isEqual:self.status];
                  }]? YES: NO;
                  }

                  очень удобно. Получается что-то вроде функционального стиля.
                  Не знаю, что бы я делал без категорий :)
                  • 0
                    Насмотрелся «плохих» категорий, да и любую задачу можно решить без их использования. Мне тяжело понять _зачем_ ими пользоваться :)
                    Я и сам ими пользуюсь, но только там, где, как мне кажется, это уместно.
                    А вообще тема довольно холиварная, на вкус и цвет :)
                    • +1
                      На вскус и цвет — это да. У меня большинство наработок, которые кочуют из проекта в проект выполнено в виде категорий.

                      Приведу свои аргументы «зачем». Например, мне кажется, удобнее, чем наследоваться от NSURL или писать дополнительный класс, поиметь у того же NSURL методы URLWithoutAnchor, URLWithoutParameters, — (NSURL *)URLByModifyingParams:(void(^)(NSMutableDictionary *params))modification.

                      Конечно, мы можем воспользоваться обычной функцией и передавать объект первым аргументом. Но кажется a.b красивее, чем b(a) и [x y:z p:q] красивее, чем yp(x, z, q).

                      У категорий есть полезное ограничение — отсутствие инкапсулированного состояния **. Т.е. если вы решили расширить сущность и instance variable не требуется, категории — красивый способ это сделать. Видишь унаследованный класс — сразу понимаешь, что внутри может быть какое-то состояние. Видишь категорию — только дополнительные методы.

                      хороший пример — BlocksKit. Например, UIAlertView — github.com/pandamonia/BlocksKit/blob/master/BlocksKit/UIKit/UIAlertView%2BBlocksKit.h

                      ** — на самом деле мы можем любой стейт ассоциировать с любым объектом в рантайме и обойти это ограничение, но это не так удобно, неочевидно и требует objc/runtime.h
                  • 0
                    Мне, например, по-кайфу писать код вроде: pageURL = rawURLString.URL.URLWithoutAnchor.URLWithoutParameters;

                    В данном случае уровень вложенности зашкаливает.
          • 0
            Буквально вчера была добавлена возможность использовать протоколы, не только конкретные классы
            • 0
              Раз lazy_initializer исполняется вне main, то надо бы его код обернуть в @autoreleasepool{...}, иначе очень легко допустить утечку.

              Я для той же цели обычно реализую метод +load у AppDelegate, в Obj-C это как-то привычнее.
              • 0
                Да, вы правы.
                Для метода +load нужно создавать класс, а lazy_initializer позволяет иметь только один .m файл.

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