АОП в Objective-C: внедрение контекстно-зависимого кода в каждый метод при сохранении DRY

ОБНОВЛЕНИЕ:

Обратившись к Джорджу с некоторыми ключевыми предложениями, я придумал два разных способа добиться именно того, чего я хочу, в CodeRunner, и разместил их на основном сайте Github: Objective-C AOP gist

Код грубый, потому что это новая концепция, а я закончил только в 1:30. Это определенно работает и имеет некоторые тонкости, такие как автоматическое добавление всех методов, которые не являются инициализаторами, геттерами или сеттерами. [КОНЕЦ ОБНОВЛЕНИЯ]

Несколько раз (но, конечно, не очень часто) я сталкивался с ситуацией, когда мой код был бы немного DRYer, если бы я мог вызывать контекстно-зависимый фрагмент кода для каждого метода в классе. Использование среды выполнения Objective-C совершенно нормально, я бы также принял решения C или C++.

Вместо:

- (void)methodName1
{
   self->selector = _cmd;
   NSLog(@"This method is named: %@",_cmd);
   //more code
}

- (void)methodName2
{
   self->selector = _cmd;
   NSLog(@"This method is named: %@",_cmd);
   //more code
}

Сделайте что-то вроде этого, с тем же результатом:

+ (void)AOPMethod
{
   self->selector = _cmd;
   NSLog(@"This method is named: %@",_cmd);
}

- (void)methodName1
{
   //more code
}

- (void)methodName2
{
   //more code
}

В реальном приложении AOPMethod будет содержать больше кода и в классе будет больше методов.

P.S. Я довольно одержим DRY. Наряду с ясностью изложения и производительностью это ключевой компонент того, как я оцениваю качество своего кода в долгосрочной перспективе. Для каждого нового способа, которым я могу избежать повторения, выгода экспоненциальна, потому что я прерываю как можно больше кода в повторно используемых классах, которые используются во многих проектах.


person james_womack    schedule 13.02.2012    source источник
comment
Вы просто ищете перехваты входа/выхода?   -  person Georg Fritzsche    schedule 14.02.2012
comment
@GeorgFritzsche Спасибо за вопрос. Если перехват метода не зависит от платформы и позволит использовать контекстно-зависимые данные уровня метода, такие как _cmd, в каждом методе класса — и без дублирования кода — тогда да. Другими словами, если вы можете поделиться методом на одном из упомянутых базовых языков (а не на фреймворках, доступных не на всех платформах), который позволит некоторой версии последнего примера иметь тот же результат, что и первый, то это ответ. Еще раз спасибо.   -  person james_womack    schedule 14.02.2012
comment
Для вашего конкретного варианта использования (все методы с одинаковой сигнатурой) этот подход можно расширить, чтобы исправить все подходящие методы в списке методов. На данный момент я не могу придумать элегантного более общего решения, только неэффективные.   -  person Georg Fritzsche    schedule 14.02.2012
comment
@GeorgFritzsche Я думаю, что какая-то версия предложенной вами техники по предоставленной вами ссылке может сработать. Однако я провел некоторый тест и не нашел способа сделать один или два вызова, которые настраивают это для каждого метода. В конце концов, я думаю, что использование NSProxy может быть чище.   -  person james_womack    schedule 15.02.2012
comment
То, что я связал, всегда требует наличия метода перехватчика (то есть того, который будет вызывать обработчики до и после) с сигнатурой метода нужных вам методов. Единственное более общее решение, которое я могу придумать, - это обработать его с помощью NSInvocations, но для вызова forwardInvocation: вам нужно удалить исходные методы (больше невозможно в ObjC 2). Я думаю, что NSProxy стэндины — это лучшая вещь, не прибегающая к хакам.   -  person Georg Fritzsche    schedule 15.02.2012
comment
@GeorgFritzsche Благодаря вашему руководству и моей тяжелой работе я разработал рабочие решения, используя либо NSProxy + свойства блоков ввода/вывода + прокручивание, либо кэширование IMP во время выполнения + логический оператор (для включения или выключения входа/выхода) + шипение. Я инкапсулировал оба способа достижения АОП в подклассе NSProxy для максимальной гибкости. Мой класс интеллектуально предотвращает дублирование методов инициализации, геттеров и сеттеров с помощью префикса (префикс является частью того, как работает кэширование/пересылка методов IMP). Не стесняйтесь перемещать свои комментарии в ответ, чтобы я предоставил вам репутацию.   -  person james_womack    schedule 15.02.2012


Ответы (1)


Для конкретного варианта использования в вопросе можно предоставить обработчик, который заменяет исходные функции реализации и вызывает обработчики до/после, а также исходные функции, используя что-то вроде этот подход. В целом, однако, исправление реализации метода не будет работать, поскольку для каждой перехваченной сигнатуры метода необходимо будет предоставить метод обработчика/перехвата.

Что будет работать в более общем плане (т. е. для всего, кроме функций с переменными аргументами), будет обрабатывать -forwardInvocation:. Однако проблема здесь в том, что нам нужно было бы вызвать этот метод в первую очередь. Поскольку мы не можем удалить методы в ObjC2, это невозможно сделать на месте.

Что можно сделать, так это использовать прокси, которые реализуют forwardInvocation: и вызывают наши обработчики до/после.

@interface AspectProxy : NSProxy {
    id target_;
}
- (id)initWithTarget:(id)target;
@end

@implementation AspectProxy
- (id)initWithTarget:(id)target {
    target_ = [target retain];
    return self;
}
- (void)dealloc {
    [target_ release];
    [super dealloc];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [target_ methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)inv {
    SEL sel = [inv selector];
    NSLog(@"forwardInvocation for: %@", NSStringFromSelector(sel));
    if (sel == @selector(aspectBefore:) || sel == @selector(aspectAfter:)) {
        return;
    }
    if ([target_ respondsToSelector:@selector(aspectBefore:)]) {
        [target_ performSelector:@selector(aspectBefore:) withObject:inv];
    }
    [inv invokeWithTarget:target_];
    if ([target_ respondsToSelector:@selector(aspectAfter:)]) {
        [target_ performSelector:@selector(aspectAfter:) withObject:inv];
    }
}
@end

Поскольку нам не нужно возвращать фактический экземпляр из метода init, это можно сделать даже прозрачно:

@interface Test : NSObject
- (void)someFunction;
@end

@implementation Test
- (id)init {
    if (self = [super init]) {
        return [[AspectProxy alloc] initWithTarget:[self autorelease]];
    }
    return self;
}
- (void)aspectBefore:(NSInvocation *)inv {
    NSLog(@"before %@", NSStringFromSelector([inv selector]));
}
- (void)aspectAfter:(NSInvocation *)inv {
    NSLog(@"after %@", NSStringFromSelector([inv selector]));
}
- (void)someFunction {
    NSLog(@"some function called");
}
@end

Теперь следующий код:

Test *x = [[[Test alloc] init] autorelease];
[x someFunction];

... напечатает:

forwardInvocation для: someFunction
перед someFunction
некоторой функцией, вызываемой
после someFunction

Запускаемый пример можно найти в этой сути.

person Georg Fritzsche    schedule 15.02.2012
comment
Это действительно крутое решение. Однажды я сделал что-то подобное, чтобы создать делегат-прокси, который позволяет мне не писать if ([delegate respondsToSelector:]) {} для необязательных методов. Работает отлично! - person Alex; 15.02.2012
comment
В итоге я использовал forwardInvocation & NSProxy в качестве одного из вариантов (о котором я узнал вскоре после публикации вопроса), но был более одержим использованием среды выполнения, поэтому я также придумал способ сделать это. Оба метода связаны с сутью моего вопроса. Мне нравится, насколько чиста ваша версия NSProxy. - person james_womack; 16.02.2012