Pull to refresh

Паттерны проектирования для iOS разработчиков. Observer, часть I

Reading time 8 min
Views 22K

Вместо предисловия


Прошло уже 17 лет с тех пор, как вышла легендарная книга Банды Четырех, посвященная Паттернам проектирования (Design patterns). Несмотря на столь солидный срок, тяжело оспорить актуальность описанных в ней методик. Паттерны проектирования живут и развиваются. Их применяют, обсуждают, ругают и хвалят. К сожалению, для многих они до сих пор остаются излишней абстракцией.

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


Паттерн Observer


Пример из жизни

Студент Вася очень любит ходить на вечеринки. Если быть точнее, они заняли настолько важную часть его жизни, что его выгнали из института. Занявшись поиском работы, Вася понял, что почти ничего не знает и не умеет. Хотя, подождите… У Васи есть очень много знакомых красивых девушек, которых хлебом не корми — дай проникнуть на классную тусовку без приглашения. Также Васю знают во всех соответствующих заведениях его города. Наконец, Вася понимает: чтобы вечеринка удалась (обеспеченные посетители потратили много денег), хорошо бы наполнить ее красивыми девушками (на которых эти деньги будут потрачены).

Так Вася стал сутенером открыл свой бизнес. Владельцы заведений обращаются к Васе (кто же не знает Васю?!) с новостями о модных закрытых мероприятиях. Вася сообщает своим знакомым девушкам о том, что он может их туда провести. Откуда же он берет девушек? Все просто: возьмем Свету. Она познакомилась с Васей в клубе на прошлой неделе. Света давно мечтала побывать на какой-нибудь элитной тусовке, поэтому попросила Васю записать ее номер телефона. Девушка она симпатичная, одета совсем недурно, так что Вася согласился. Так Света подписалась на Васины услуги и стала наблюдателем. Впрочем, Вася предупредил ее, что позвонит он сам, когда придет время.

Прошел год, Света закончила институт, нашла себе работу и, главное, вышла замуж! Ей больше не хочется ходить на вечеринки, поэтому вчера она позвонила Васе и попросила не донимать ее больше своими идиотскими сообщениями. Так она отписалась от (весьма подозрительного) субъекта Васи. Впрочем, она знает, что всегда может подписаться снова (она все еще симпатичная, молодая, хорошо одевается).

Тем временем, Вася не стоял на месте, а расширял бизнес. Недавно он познакомился с несколькими весьма обеспеченными футболистами, которые любят расслабиться на вечеринке и всегда готовы угостить симпатичных девушек безалкогольным коктейлем. Естественно, их тоже можно приглашать на разного рода мероприятия (организаторы в восторге, платят Васе больше денег). Расширение бизнеса прошло незаметно: действительно, какая разница, кому звонить? И у девушек, и у футболистов есть телефоны. Иногда, сообщая о новом мероприятии, Вася даже путает, с кем он разговаривает.

Пример покороче

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

Определение

Банда Четырех

  • Название: Наблюдатель.
  • Классификация: Паттерн поведения.
  • Назначение:
    Определяет зависимость типа один ко многим между объектами таким образом, что при изменении состояния одного объекта все зависящие от него оповещаются об этом и автоматически обновляются.


Комментарии

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

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

Структура

image

Наблюдатель для iOS разработчика


Давайте перейдем к программированию. Сначала мы разберем собственную реализацию паттерна наблюдатель на конкретном примере, а потом разберем механизм оповещений, реализованный в Cocoa.

Собственная реализация

Допустим, мы разрабатываем совершенно новую игру, которая непременно взорвет App Store. После прохождения очередного уровня нам нужно сделать две вещи:
  1. Показать поздравительный экран.
  2. Открыть доступ к новым уровням.

Стоит отметить, что первое и второе действия никак между собой не связаны. Мы можем выполнять их в произвольном порядке. Также мы подозреваем, что в скором времени понадобится расширить число таких действий (например, мы захотим посылать какие-то данные на свой сервер или проверить, не заработал ли пользователь достижение в Game Center).

Применим изученный паттерн наблюдателя. Начнем с протокола наблюдателя.

@protocol GameStateObserver <NSObject>
 
- (void)completedLevel:(Level *)level withScore:(NSUInteger)score;
 
@end


Здесь все довольно прозрачно: наблюдатели будут реализовывать протокол GameStateObserver. Разберемся с субъектом.

@protocol GameStateSubject <NSObject>
 
- (void)addObserver:(id<GameStateObserver>)observer;
- (void)removeObserver:(id<GameStateObserver>)observer;
- (void)notifyObservers;
 
@end


Перейдем к интересующим нас классам. Пусть состояние текущей игры хранится в объекте класса GameState. Тогда его определение будет выглядеть примерно так:

@interface GameState : NSObject <GameStateSubject> {
    …
    NSMutableSet *observerCollection;
}
 
@property (readonly) Level *level;
@property (readonly) NSUInteger score;
 

- (void)updateState;
 
@end


При этом мы считаем, что метод updateState вызывается каждый раз, как в игре происходят существенные изменения. Приведем часть реализации GameState:

@implementation GameState
 

- (void)addObserver:(id<GameStateObserver>)observer {
    [observerCollection addObject:observer];
}
 
- (void)removeObserver:(id<GameStateObserver>)observer {
    [observerCollection removeObject:observer];
}
 
- (void)notifyObservers {
    for (id<GameStateObserver> observer in observerCollection) {
        [observer completedLevel:self.level withScore:self.score];
    }
}
 
- (void)updateState {
    …
    if (levelCompleted) {
        [self notifyObservers];
    }
}
 
@end


Теперь всякому объекту, которому необходимо знать об успешном проходжении уровня, достаточно реализовать протокол GameStateObserver и подписаться на оповещение об успешном завершении. Соотвествующий код будет выглядеть примерно так:

GameState *gameState = [[GameState alloc] init];
[gameState addObserver:levelManager];
[gameState addObserver:levelViewController];


Здесь levelViewController — контроллер, отвечающий за интерфейс игрового процесса, а levelManager — объект модели, отвечающий уровни.

Обсуждение

Мы рассмотрели довольно примитивный пример, который, однако, встречается повсеместно. Сразу же видно, что решение достаточно гибкое. Стоит отметить, что в оповещении мы решили передавать в качестве параметров некоторые данные. У такой реализации есть как свои плюсы, так и минусы. Может оказаться удобным использовать следующий вариант протокола GameStateObserver:

@protocol GameStateObserver <NSObject>
 
- (void)levelCompleted:(id<GameStateSubject>)subject;
 
@end


Соответствующий вариант GameStateSubject выглядел бы так:

@protocol GameStateSubject <NSObject>
 
- (void)addObserver:(id<GameStateObserver>)observer;
- (void)removeObserver:(id<GameStateObserver>)observer;
- (void)notifyObservers;
 
@property (readonly) Level *level;
@property (readonly) NSUInteger score;
 
@end


Наблюдатель в Cocoa: Notifications

Оказывается, в Cocoa есть механизм, позволяющий реализовать паттерн наблюдателя. Если быть совсем точным, таких механизмов два. В данный момент мы остановимся на механизме оповещений, а второй оставим на будущее. Далее мы не будем вдаваться во все тонкости, а лишь опишем базовую функциональность.

Неформально, механизм оповещений позволяет делать две вещи: подписываться/отписываться от оповещения и разослать оповещение всем подписчикам. Оповещение представляет из себя экземпляр класса NSNotification. Оповещения задаются своим строковым именем name типа NSString. Помимо имени, оповещение содержит также субъект object и дополнительные данные userInfo типа NSDictionary.

За подписки и доставку оповещений отвечает NSNotificationCenter. Чтобы получить к нему доступ, в большинстве случаев достаточно вызвать классовый метод defaultCenter.

Чтобы подписаться на сообщение, у центра оповещений имеется метод addObserver:selector:name:object:. Первым параметром выступает наблюдатель, вторым — селектор, который будет вызываться при оповещении. Он должен иметь сигнатуру - (void)methodName:(NSNotification *). Третий параметр — имя оповещения, четвертый — субъект. Если в качестве субъекта передать nil, то оповещение будет доставляться от произвольного отправителся (при условии совпадения имени). С использованием оповещений код подписки выглядел бы так:

GameState *gameState = [[GameState alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:levelManager
    selector:@selector(levelCompleted:)
    name:@"LevelCompletedNotification" object:gameState];
[[NSNotificationCenter defaultCenter] addObserver:levelViewController
    selector:@selector(levelCompleted:)
    name:@"LevelCompletedNotification" object:gameState];


Примерный вид метода levelCompleted:

- (void)levelCompleted:(NSNotification *)notification {
    id<GameStateSubject> subject = [notification object];
    Level *level = subject.level;
    NSUInteger score = subject.score;
}


Вообще говоря, от протокола GameStateSubject можно избавиться и использовать напрямую GameState.

Чтобы отписаться от оповещений, нужно вызвать один из методов removeObserver: или removeObserver:name:object:.

Отправка оповещения — процесс еще более простой. На то имеются методы postNotificationName:object: и postNotificationName:object:userInfo:. Первый выставляет значение userInfo нулевым nil. Новая реализация метода notifyObservers следует.

- (void)notifyObservers {
    [[NSNotificationCenter defaultCenter]
        postNotificationName:@"LevelCompletedNotification" object:self];
}


Комментарии

Механизм посылки оповещений умеет гораздо больше, чем мы описали. Например, весь приведенный код — синхронный. Для асинхронной отправки оповещений нужно использовать очереди оповещений NSNotificationQueue.

Также стоит отметить, что все описанное работает как для Cocoa, так и для Cocoa Touch. В дальнейшем мы все же будем использовать некоторые особенности платформы iOS.

Вместо послесловия


Давайте подведем некоторый итог тому, что мы рассмотрели и узнали. Итак, мы:
  • изучили паттерн проектирования наблюдатель;
  • использовали его собственную реализацию в часто встречающейся ситуации;
  • научились делать то же самое с помощью механизма оповещений в Cocoa;
  • узнали, что паттерны проектирования — это не страшно, не сложно и не громоздко;
  • осознали, что паттерны берутся не с потолка, а из реальной жизни (вспомним сутенера промоутера Васю)!

Во второй части мы узнаем про механизм Key-Value Observing, который также реализует паттерн наблюдатель. Впереди еще много паттернов!

Полезные источники


  • Э. Гамма, Р. Хелм, Р. Джонсон, Дж. Влиссидес Приемы объектно-ориентированного проектирования. Паттерны проектирования = Design Patterns: Elements of Reusable Object-Oriented Software. — СПб: «Питер», 2007. — С. 366. — ISBN 978-5-469-01136-1 (также ISBN 5-272-00355-1)
  • Э. Фримен, Э. Фримен, К. Сьерра, Б. Бейтс Паттерны проектирования = Head First Design Patterns — СПб: «Питер», 2011. — С. 656. — ISBN 978-5-459-00435-9
  • Notification Programming Topics — iOS Developer Library
Tags:
Hubs:
+30
Comments 18
Comments Comments 18

Articles