Objective C. Практика. События

    Событийно-ориентированная логика в Objective C держится на трех китах — протоколы, notification center и key-value observing. Традиционо протоколы используются для расширения функционала базовых классов без наследования, key-value observing – для взаимодействия между визуальной и логической частью приложения, а notification center — для обработкий событий пользователя.

    Естественно, все это благообразие можно спокойно использовать для построения сложных приложений. Никакой реальной необходимости в изобретении собственных велосипедов, конечно же, нет. Однако мне, как человеку пришедшему в разработку Objective C приложений из мира .NET, показалось очень неприятным то, что notification center, который я планировал использовать для событий, разраывает стек приложения, записывая произошедшее событие в очередь в UI thread, а протоколы в классическом представлении не слишком удобны, посему для удобства я решил соорудить себе механизм, который был бы гораздо больше похож на то, чем мы привыкли обходиться в мире .NET. Так родился родилась идея реализации модели множественных подписантов через специальный класс, названный AWHandlersList.

    Данная статья рассчитана на программистов, которые имеют определенный опыт в создании приложений на Objective C и уже писали подобные велосипеды, либо решали похожие задачи стандартными способами. Данный вариант не является silver bullet, но показал себя как удобный механизм, минимизирующий написание кода для обарботки множеств событий с разными интерфейсами и параметрами.

    Идея класса довольно проста — он содержит в себе список подписантов, каждый элемент которого состоит из двух компонентов — target и selector.

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

    В .NET привычная модель событийной логики предлагает делегат с двумя параметрами — sender типа object и args типа, наследуемого от EventArgs. Чтобы не ломать себе мозг, сделаем то же самое. Для начала, определим пустой класс EventArgs, от которого будут наследоваться все аргументы событий.

    @interface AWEventArgs : NSObject
    
    @end


    Теперь определим класс, который будет содержать пару «целевой объект и вызываемый метод», добавив туда заодно некоторую отладочную информацию для того, чтобы в дальнейшем можно было легко отлаживать логику событий.

    @interface AWEventHandler : NSObject
    {
    	@private
    	NSString *_description;
    }
    
    @property (nonatomic, assign) id target;
    @property (nonatomic, assign) SEL method;
    
    +(AWEventHandler *)handlerWithTarget:(id)target method:(SEL)method;
    
    @end
    
    @implementation AWEventHandler
    
    @synthesize method, target;
    
    -(id)initWithTarget:(id)t method:(SEL)m;
    {
    	self = [super init];
    	if(self)
    	{
    		target = t;
    		method = m;
    		_description = [[NSString alloc] initWithFormat:@"EventHandler, Target=%@, Method=%@", NSStringFromClass([target class]), NSStringFromSelector(method)];
    	}
    	return self;
    }
    
    -(NSString *)description
    {
    	return _description;
    }
    
    -(void)dealloc
    {
    	[_description release];
    	
    	[super dealloc];
    }
    
    +(AWEventHandler *)handlerWithTarget:(id)target method:(SEL)method
    {
    	AWEventHandler *handler = [[[AWEventHandler alloc] initWithTarget:target method:method] autorelease];
    	return handler;
    }
    
    @end


    Как видите, и target, и method по сути представляют собой слабые ссылки. Это вполне закономерно — слабые ссылки повсеместно используются в мире Objective C для того, чтобы избежть circular references и дать возможность автоматически освобождать объекты. К сожалению, это приводит к тому, что при небрежном кодировании повсеместно появляются «мертвые» указатели на объекты, которые роняют приложение, поэтому я чуть дальше покажу один красивый механизм, который позволяет предупреждать и устранять их появление.

    Теперь, наконец, перейдем к нашему основному классу — списку подписантов. В коде есть нетривиальные моменты, но они решаются чтением документации, а если желания разбираться в вопросе нет — то его можно просто использовать, код полностью рабочий и вынут из «боевого» проекта.

    @interface AWEventHandlersList : NSObject
    {
    	NSMutableArray *_handlers;
    }
    
    @property (nonatomic, copy) NSString *name;
    
    -(void)addReceiver:(id)receiver delegate:(SEL)delegate;
    -(void)removeReceiver:(id)receiver delegate:(SEL)delegate;
    
    -(void)clearReceivers;
    -(void)invoke;
    -(void)invokeWithSender:(id)sender;
    -(void)invokeWithSender:(id)sender args:(AWEventArgs *)event;
    
    @property (nonatomic, retain) NSRunLoop *runLoop;
    
    @end


    Вкратце поясню, зачем нужны поля данного класса.

    Первое — это name. Я предпочитаю именовать события, чтобы можно было увидеть в логах, какое именно событие было вызвано. Обычно в качестве имени события я использую имя класса вкупе с именем вызываемого в нем для выбрасывания (raise) метода. Это удобная практика, так как позволяет не рыскать судорожно по стеку в поисках того, кто событие выбросил, а просто в консоли отладки посмотреть это значение.

    Методы addReceiver и removeRecevier логичны — они принимают объект и селектор, которые в дальнейшем будут принимать вызовы.

    Методы invoke должны выбрасывать событие, передавая его для обработки в подписанные объекты. Они даются в трех вариантах — для того, чтобы не передавать пустые значения nil в том случае, если в каикх-то параметрах события нет нужды.

    Метод clearReceivers внутренний, его лучше определять в анонимной секции, так как вызывающий код не должен иметь возможности отписывать другие объекты от событий, но исторически сложилось так, что он вынесен в интерфейс. Это легко исправить, если вам кажется это неправильным.

    Наконец, свойство runLoop необходимо в том случае, если вы собираетесь делать так, чтобы некоторые события были привязаны к определенному потоку (thread). Например, это необходимо, если существует какой-то код в worker thread должен обновлять визуальную часть приложения, либо наоборот — из UI thread должен быть доступ к какому-либо worker thread, синхронизируемому через очередь сообщений, то есть если есть необходимость выбрасывать события и обрабатывать их в разных потоках.

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

    @implementation AWEventHandlersList
    
    @synthesize runLoop = _runLoop;
    
    @synthesize name = _name;
    
    -(id)init
    {
    	self = [super init];
    	if(!self)
    		return nil;
    	
    	_handlers = [[NSMutableArray alloc] init];
    	
    	return self;
    }
    
    -(void)addReceiver:(id)receiver delegate:(SEL)delegate
    {
    	/* Этот код можно убрать, если вы гарантируете, что каждый объект будет подписываться на событие только один раз, либо
    	 * вам необходимо множественное подписание. Я предпочитаю работать со страховкой */
    	[self removeReceiver:receiver delegate:delegate]; 
    
    	[_handlers addObject:[AWEventHandler handlerWithTarget:receiver method:delegate]];
    }
    
    -(void)removeReceiver:(id)receiver delegate:(SEL)delegate
    {
    	/* В идеале снятие копии со списка, сделанное для поддержки многопоточности, должно производитсья в критической секции
    	 * (NSLock), однако я опустил этот момент, как как у меня подписание на события всегда происходит в одном потоке, 
    	 * а копия списка берется для того, чтобы в будущем достаточно было обернуть вызов в NSLock */
    	for(AWEventHandler *handler in [[_handlers copy] autorelease])
    		if(handler.method == delegate && handler.target == receiver)
    			[_handlers removeObject:handler];
    }
    
    -(void)clearReceivers
    {
    	[_handlers removeAllObjects];
    }
    
    -(void)invoke
    {
    	[self invokeWithSender:nil args:nil];
    }
    
    -(void)invokeWithSender:(id)sender
    {
    	[self invokeWithSender:sender args:nil];	
    }
    
    -(void)invokeWithSender:(id)sender args:(AWEventArgs *)event
    {
    	[self invokeWithSender:sender args:event runLoop:_runLoop];
    }
    
    -(void)invokeWithSender:(id)sender args:(AWEventArgs *)event runLoop:(NSRunLoop *)runLoop
    {
    	/* Вс случае, если к текущему потоку не привязан цикл выборки сообщений, метод вернет null и выполнеие будет
    	 * происходить по обычному сценарию */
    	if(!runLoop)
    		runLoop = [NSRunLoop currentRunLoop];
    	
    	NSUInteger order = 1;
    	NSArray *handlersCopy = [NSArray arrayWithArray:_handlers];
    	for(AWEventHandler *handler in handlersCopy)
    		if(runLoop == [NSRunLoop currentRunLoop])
    			[self internalInvoke:[NSArray arrayWithObjects:handler, sender == nil ? [NSNull null] : sender, event == nil ? [NSNull null] : event, nil]];
    		else
    			[runLoop performSelector:@selector(internalInvoke:) target:self argument:[NSArray arrayWithObjects:handler, sender == nil ? [NSNull null] : sender, event == nil ? [NSNull null] : event, nil] order:order++ modes:[NSArray arrayWithObject:NSDefaultRunLoopMode]];
    }
    
    /* Передача объектов производится через массив для возможности работы с потоками через performSelector:target:argument:order:modes: */
    -(void)internalInvoke:(NSArray *)data
    {
    	AWEventHandler *handler = [data objectAtIndex:0];
    	
    	id sender = [data objectAtIndex:1];
    	if(sender == [NSNull null])
    		sender = nil;
    	
    	id args = [data objectAtIndex:2];
    	if(args == [NSNull null])
    		args = nil;
    	
    	/* Данный класс используется для анализа сигнатуры метода и определения потребного числа параметров его вызова */
    	NSMethodSignature *mSig = [handler.target methodSignatureForSelector:handler.method];
    	if([mSig numberOfArguments] == 2)
    		[handler.target performSelector:handler.method];
    	else if([mSig numberOfArguments] == 3)
    		[handler.target performSelector:handler.method withObject:sender];
    	else if ([mSig numberOfArguments] == 4)
    		[handler.target performSelector:handler.method withObject:sender withObject:args];
    	else
    		@throw [NSException exceptionWithName:@"Invalid selector type" reason:@"This type of selector is not supported" userInfo:nil];
    }
    
    -(void)dealloc
    {
    	self.name = nil;
    
    	[self clearReceivers];
    	[_handlers release];
    
    	[super dealloc];
    }
    
    @end


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

    #define DEFINE_EVENT(eventName) \
    	-(void)add ## eventName ## Handler:(id)receiver action:(SEL)action; \
    	-(void)remove ## eventName ## Handler:(id)receiver action:(SEL)action 
    
    #define DEFINE_EVENT_IMPL(eventName, innerVariable) \
    	-(void)add ## eventName ## Handler:(id)receiver action:(SEL)action \
    	{ \
    		[innerVariable addReceiver:receiver delegate:action]; \
    	} \
    	\
    	-(void)remove ## eventName ## Handler:(id)receiver action:(SEL)action \
    	{ \
    		[innerVariable removeReceiver:receiver delegate:action] ; \
    	} \


    Теперь для того, чтобы создать в классе событие, нужно определить внутреннюю списочную переменную:

    AWEventHandlersList *_handlers;


    Определить событие в интерфейсе

    DEFINE_EVENT(Event);


    И связать список с событием

    DEFINE_EVENT_IMPL(Event, _handlers)


    В классе автоматически добавляются два метода — addEventHandler:action: и removeEventHandler:action:, а вызвать событие можно через методы invoke объекта _handlers.

    Конечно, не стоит забывать о том, что объект _handlers нужно инициализировать в конструкторе

    _handlers = [AWEventHandlersList new];


    И уничтожать в деструкторе объекта

    [_handlers release];


    Во второй части статьи я расскажу, к каким проблемам ведет использование этого подхода и как справляться с трудностями «мертвых» ссылок, которые возникают в любом мало-мальски объемном приложении в результате наших собственных ошибок.
    Поделиться публикацией
    Похожие публикации
    Ой, у вас баннер убежал!

    Ну, и что?
    Реклама
    Комментарии 20
    • +1
      Где же ссылка на GIT? Охота переписать это дело на ARC. Код годный, хотя у ребят, которые пришли в айфон не из C# может вызвать панику и бугурт — айфонщики очень любят средства из коробки. И на то есть причина — в основном, реализации всяких там DAO, и других паттернов мудрых выглядят на Objective-C убого, ведь он как Ruby — чем проще, тем лучше. Друго дело, если таким велосипедом решается проблема синхронизации.
      • 0
        Я гитом не пользуюсь, потому что не свезло — мы на работе пользуемся TFS и SVN.

        Если вам интересно — можете опубликовать на своем, я добавлю ссылку в топик. Лицензии нет, так что можно считать, что это выкладывается под BSD.
      • +1
        Я вот не совсем понял вот это «Событийно-ориентированная логика в Objective C держится на трех китах — протоколы, notification center и key-value observing», а точнее при чем тут протоколы? Ну и причины побудившее Вас к написанию «велосипеда» тоже описаны расплывчато и опять при чем тут протоколы и, раз уж они Вам не нравятся, то где в статье другие методы расширения классов?
        • 0
          На базе протоколов тоже замечательно реализуется событийно-ориентированная логика.

          А причины мутные именно в силу того, что тут вопрос вовсе не в том, что так надо или не надо, а в том, как привычнее. Мне привычен и удобен данный подход, хотя в зависимости от места я использую и подписание через KVO, и нотификации, и проброс событий с помощью протоколов.
        • +1
          Спасибо за свежий взгляд со стороны! Но стандартные notifications вы явно недооценили. Стек они вовсе не разрывают, если конечно вы не посылаете их асинхронно. И отрабатывают они в том же потоке, из которого они были посланы, а не обязательно в main thread.
          Критиковать не сложно, попробую вас поддержать! Что мне не нравится в стандартных notifications, так это объемный синтаксис и то, как передаются параметры. Вот если бы посылать сообщение с произвольным числом типизированных параметров, и через message forwarding пересылать подписчикам — может получиться что-то интересное…
          • 0
            В моем подходе произвольное число типизированных параметров пересылается через наследники класса AWEventArgs, которые по необходимости определяются для каждого события.

            Хотелось унифицировать интерфейс, не потеряв гибкости.
          • +3
            Событийно-ориентированная логика в Objective C держится на трех китах — протоколы, notification center и key-value observing.


            А как же блоки? и не протоколы, раз уж на то пошло, а делегирование.
            • 0
              Блоки — это техническая вещь, представляющая собой фактически анонимный метод, я не думаю, что следует обращать особое внимание на это.
              • +2
                Блоки — это техническая вещь, представляющая собой фактически анонимный метод

                Это не так, и именно в этом и заключаются мощные возможности блоков для обработки событий
                • +1
                  Расскажите мне, что имеется ввиду.
                  • +2
                    Блоки являются замыканиями, поэтому позволяют обрабатывать события в стиле javascript:
                    SomeClass *someObject = self.someObject;
                    
                    [object on:@"EventName" do:^(Event *event) {
                        [someObject doSomething];
                    }];
                    
                    • 0
                      Да, можно и так.

                      Кстати, мой код несложно расширить для того, чтобы он брал блоки в качестве обработчиков события. Он даже проще станет.
            • +2
              Кроме очень мутной задачи, которую Вы решали (догадываюсь, что Вам бы помог NSNotificationQueue, который отлично описан в документации) мне не нравится использование названия delegate. Вы что-то кому-то делегируете? Нет. Очень часто вижу в коде разных исходников delegate к месту и не к месту.
              • –1
                В .NET для указателей на методы класса используется понятие «делегат», который фактически представляет собой пару target+method то есть является фактически полноценным method as the first class object.

                Проблемы с терминологией решаются автозаменой.
                • +2
                  И вообще часто сталкиваюсь с тем, что кто-то пришел с другого языка и тянет за собой все с формулировкой «Я так привык». Может лучше привыкнуть/переучиться на стандартные методы для платформы? Ну сделали вы велосипед, а как теперь его использовать для системных классов? Будем где использовать NSNotificationCenter, а где-то Ваш код? А третий человек придет из языка _новый_язык_ и тоже добавит в этот же проект еще одну систему, так как он к ней «привык»?
                  • –1
                    Может быть, имеет смысл переучиться на другую технику. А может быть и нет — здесь непонятно, как правильно.

                    Я в C# добавляю монады, например. Я вообще стараюсь тянуть из разных языков наиболее удобные подходы, чтобы сделать свою жизнь проще.
                • +1
                  Вместо велосипеда с NSArray для групировки объекта, селектора и аргументов лучше использовать NSInvocation и пересылать в нужный runLoop вызов invoke.
                • 0
                  блин, как это всё сложно для новичка — просто капец

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

                  Самое читаемое