Разделения на 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/
Добавить комментарий