NSProxy, как способ срезать на поворотах

в 10:36, , рубрики: iOS SDK, nsproxy, objective-c, xcode, разработка под iOS

Как многие читали в книгах, в языке Objective-C изначально есть два корневых класса — NSObject и NSProxy. И если на первом основано практически все и с ним невозможно не столкнуться, то вторым пользуются значительно реже. В этой небольшой статье я опишу те применения этого класса, которые приходилось использовать мне.

В качестве первого примера, напомню о такой штуке, как UIAppearance — насколько мне известно — единственное использование NSProxy в базовых iOS фреймворках. Его задача заключается в предварительном конфигурировании группы UIKit объектов в одном месте. Фактически, вы описываете некоторый набор действий, который будет применяться к каждому создаваемому объекту (таких как задание цвета, шрифта и других), удовлетворяющему некоторым условиям (сейчас таких условий два: класс объекта и класс объекта, содержащего наш объект как subview). Есть замечательная статья, посвященная использованию, возможностям и побочным эффектам такого инструмента, поэтому более не будем на этом останавливаться.

Если честно, это не густо. Для такого гордого статуса, как «один из двух корневых классов», слишком мало примеров конструктивного использования. По своему личному опыту и опыту моих знакомых разработчиков — это достаточно сильно затрудняет начальное понимание этой сущности и, тем самым, как бы отговаривает нас от ее использования. Но интересно же! И со временем стали появляться задачи, для которых NSProxy является потрясающим по удобству инструментом.

Декорирование объектов

В реализации паттерна «декоратор» есть один достаточно неудобный аспект — интерфейс, собственно, декоратора. Если у нас имеется некоторая иерархия объектов, например такая

@interface SomeClass : NSObject <SomeClassInterface>
@end

@interface ChildClass : SomeClass
-(void) additionalMethod;
@end

@interface DecoratorClass : NSObject <SomeClassInterface>

...

ChildClass *instance = [ChildClass new];
id decoratedInstance = [DecoratorClass decoratedInstanceOf:instance]

То по понятным причинам decoratedInstance уже не сможет выполнить additionalMethod. И нам остается либо писать категории, либо внедрять в корневой класс какие-то хуки, либо заниматься еще каким-то подобным непотребством. Теперь посмотрим, как это можно решить, используя NSProxy.

@interface SomeClass : NSObject;
-(int) getNumber;
-(NSString*) getString;
@end


@interface SimpleDecorator : NSProxy
@property (nonatomic, strong) SomeClass *instance;
+(instancetype) decoratedInstanceOf:(SomeClass*)instance;
@end

@implementation SimpleDecorator

-(instancetype) initWithObject:(SomeClass*)object
{
    /* маленькое напоминание - NSProxy не имеет встроенного инициализатора, как NSObject. Поэтому вызов [super init] не нужен*/
    _instance = object;
    return self;
}

+(instancetype) decoratedInstanceOf:(SomeClass*)instance
{
    return [[self alloc] initWithObject:instance];
}

/* основные методы NSProxy - они отвечают за то, что мы делаем с теми методами, которые мы не можем обработать самостоятельно. В частности, сюда пойдут все вызовы, которые мы не декорировали - и самого класса SomeClass и его подклассов */
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
    return [self.instance methodSignatureForSelector:selector];
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    /* этой строчкой мы, фактически, говорим, что если мы не можем обработать какой-то метод, то пусть его обработает, собственно наш объект В данном случае так произойдет, например, при вызове метода getString */
    [invocation invokeWithTarget:self.instance];
}

/* собственно декорируемый метод. Так как мы можем его обработать внутри инстанса NSProxy - предыдущие два метода вызываться не будут */
- (int) getNumber
{
    return [self.instance getNumber] + 1;
}

@end

И, собственно, пример использования такой конструкции:

    SomeClass *object = [SomeClass new];
    object = (SomeClass*)[SimpleDecorator decoratedInstanceOf:object];
    NSLog(@"%d", [object getNumber]);
    NSLog(@"%@", [object getString]);

При таком подходе на месте SomeClass может быть любой из его наследник и полученный декорированный объект будет корректно откликаться на все отправленные ему сообщения.

Отложенная настройка

По сути, мы сейчас будем решать задачу, похожу на ту, которую решает UIAppearance. При работе над проектом, построенным на основе архитектуры, предлагаемой библиотектой-паттерном PureMVC ( en.wikipedia.org/wiki/PureMVC ) пришлось неоднократно сталкиваться с ситуацией, когда в какой-то точке кода мы инициируем цепочку команд-действий, результатом которых будет некоторая контекстно-зависимая сущность — например, всплывающее окно, закрытие которого запускает следующие действия, зависящие от контекста, в котором было вызвано окно. Раньше для этого использовался один из двух вариантов:
— Протяжка. Через всю цепочку действий мы передаем дополнительные, как-то структурированные данные, которые обрабатываем на месте создания окна. Довольно накладный и не очень красивый способ, особенно если некоторые действия предполагают ветвление.
— «Я все знаю». Так как PureMVC делает основные объекты, фактически, синглтонами и можно в любой момент достучаться до любого — можно попытаться получить весь необходимый контекст в команде создания всплывающего окна, что чревато значительным увеличением связности кода.

С помощью NSProxy можно добиться немного иного поведения: Выставить зависимые от контекста свойства в объект ДО того, как он был создан — в том месте, где мы знаем эти условия. Звучит, конечно, несколько абсурдно, но как-то так оно и работает. Мы обращаемся с имеющимся у нас объектом NSProxy, как с целевым объектом, которого еще нет. NSProxy хранит внутри себя действия, которые мы предприняли и когда инстанцируется объект — применяет их к нему.
Теперь немного кода.

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

/* Обычное всплывающее окно, которое показывает сообщение. */
@interface Popup : NSObject;
/* Ключ необходим для того, чтобы можно было разделять, какие отложенные сообщения кто обрабатывает */
@property (nonatomic, strong) NSString* key;
@property (nonatomic, strong) NSString* message;

/* мы не будем напрямую обращаться к прокси, обращаясь к нему только в контексте создаваемых объектов, как и в случае UIAppearance */
+(DelayedActionsProxy*) delayedInitializerForKey:(NSString*)key;
/* Показать всплывающее окошко. Собственно это место и является точкой, когда будут применены отложенные настройки */
-(void) showPopup
@end

@implementation Popup

-(void) showPopup
{
    [DelayedActionsProxy invokeDelayedInvocationsWithTarget:self];
    /*...*/
}

+(DelayedActionsProxy*) delayedInitializerForKey:(NSString*)key
{
    return [DelayedActionsProxy sharedProxyForKey:key fromClass:[self class]];
}

@end

А теперь, собственно, прокси

@interface DelayedActionsProxy : NSProxy
+(void) invokeDelayedInvocationsWithTarget:(Popup*) target;
+(instancetype) sharedProxyForKey:(NSString*)key fromClass:(Class)objectClass;
@end


@interface DelayedActionsProxy()
/* ключ нас интересует для разделения поступающих в прокси отложенных вызовов по разным объектам, а класс объекта - для корректного построения сигнатуры вызова */
@property (nonatomic, strong) NSString *currentKey;
@property (nonatomic, assign) Class currentClass;
@property (nonatomic, strong) NSMutableDictionary *delayedInvocations;
@end

@implementation DelayedActionsProxy

-(instancetype) init
{
    self.delayedInvocations = [NSMutableDictionary new];
    return self;
}

static DelayedActionsProxy *proxy = nil;
+(instancetype) sharedProxyForKey:(NSString*)key fromClass:(Class)objectClass
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        proxy = [[self alloc] init];
    });
    proxy.currentKey = key;
    proxy.currentClass = objectClass;
    return proxy;
}

/* Предполагается использование класса в виде [[Popup delayedInitializerForKey:@"key"] setText:@"someText"], то есть к моменту вызова - еще не существует вызываемого объекта, и уже заполнятся поля currentKey и currentClass */

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
    /* Так как currentClass представляет собой класс, а не объект, мы должны воспользоваться методом instanceMethodSignature вместо methodSignature */
    return [self.currentClass instanceMethodSignatureForSelector:selector];
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    if (!self.delayedInvocations[self.currentKey])
    {
        self.delayedInvocations[self.currentKey] = [NSMutableArray new];
    }
    /* мы не форвардим получаемые сообщения, а аккуратненько их складываем */
    [self.delayedInvocations[self.currentKey] addObject:invocation];
}

/* чтобы вызвать их по требованию */
+(void) invokeDelayedInvocationsWithTarget:(Popup*) target
{
    for (NSInvocation *invocation in proxy.delayedInvocations[proxy.currentKey])
    {
        [invocation invokeWithTarget:target];
    }
    [proxy.delayedInvocations removeObjectForKey:proxy.currentKey];
}

@end

И пример использования

[[Popup delayedInitializerForKey:@"key"] setText:@"someText"];

Облегчение работы с UI-объектами

В многопоточных приложениях нередко, из-за невнимательности и недостаточного планирования на начальном этапе, можно получить код, который выполняется в другом потоке, но которому страсть как нужно модифицировать UI (или БД, или еще что-нибудь весьма чувствительное к многопоточному доступу). Код начинает обрастать многочисленными performSelectorOnMainThread, dispatch_async, или того хуже — обертками над NSInvocation, поскольку performSelectorOnMainThread не дает использовать больше одного параметра. Почему бы не обзавестись единой оберткой для этого?

Пусть у нас есть какой-то объект (например объект в игре на Cocos2D)

@interface Entity : NSObject;
@property (nonatomic, strong) CCNode* node;

@end

@implementation Entity

-(void)setRepresentation:(CCNode *)node
{
    /* какое-то количество проверок корректности выставления
     ...
     */
    _node = (CCNode*)[MainThreadProxy node];
}

@end

И, собственно прокси

@interface MainThreadProxy : NSProxy
+(instancetype) proxyWithObject:(id)object;
/* это стоит вынести в отдельный метод для того, чтобы можно было делать цепочку действий с одним и тем же объектом, без постоянного форвардинга */
-(void)performBlock:(void (^)(id object))block;
@end


@interface MainThreadProxy()
@property (nonatomic, strong) id object;
@end

@implementation MainThreadProxy

-(instancetype) initWithObject:(id)object
{
    self.object = object;
    return self;
}

+(instancetype) proxyWithObject:(id)object
{
    return [[self alloc] initWithObject:object];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
    return [self.object methodSignatureForSelector:selector];
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    if ([NSThread isMainThread])
    {
        [invocation invokeWithTarget:self.object];
    }
    else
    {
        [invocation performSelectorOnMainThread:@selector(invokeWithTarget:) withObject:self.object waitUntilDone:YES];
    }
}

-(void)performBlock:(void (^)(id object))block
{
    if ([NSThread isMainThread])
    {
        block(self.object);
    }
    else
    {
        dispatch_sync(dispatch_get_main_queue(), ^{block(self.object);});
    }
}

@end

Пример использования совершенно обычен,

[entity.node render];

Но у нас есть гарантия, что это может быть вызвано совершенно корректно из любого потока.

Завершение

В качестве идей для чего можно применять прокси, можно еще выделить такие вещи как
— Маскирование удаленного объекта — работать с объектом, представляющим из себя репрезентацию какого-то сервиса с нефиксированным временем ответа (например БД на сайте, объект на клиенте, с которым ты соединен по BlueTooth), как обычный объект. Просто медленный.
— Обертка в Прокси таймера, для того, чтобы стал уместен синтаксис навроде

[[object makeCallWithTimeInterval:1.0f andRepeatCount:2] someMethod];

и другие.
Следует учитывать, что это, разумеется, не панацея. Следует быть очень аккуратным при работе с геттерами, например в приведенном выше примере строчка

[entity.node.effect update]; 

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

Post Scriptum

Вообще, о NSProxy можно почитать еще например тут, или тут. NSProxy глубоко используется в таком инструменте как OCMock. Так или иначе это достаточно гибкий и удобный инструмент, пригодный для некоторого класса задач — и с ним, как минимум, имеет смыл ознакомиться.

Автор: i_user

Источник


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


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