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

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


Прошло уже 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

Комментарии 18

    0
    Супер! Давно пора уже быть с паттернами на «ты» :)
    Теперь есть хороший материал для их изучения
      +3
      Недавно вышла неплохая книга по теме

      Pro Objective-C Design Patterns for iOS By Carlo Chung
        0
        Знаем такую! Действительно, неплохая. Примеры местами немного надуманные, но это не сильно ее портит.
        0
        Приведенный Вами в начале пример интересен только для понимания сути работы нотификаций.
        NSNotification гороздо полезнее в плане того, что не нужно объявлять протокол и поддерживать его наблюдателями.
        Наблюдатель сам укажет, какой селектор на нем должен быть вызван.
        Плюс возможность добавлять аргументы к событию (userInfo)
          0
          Пример, конечно, игрушечный. Собственную реализацию иногда писать все же полезно. Скажем, когда разрабатывается библиотека, которой потом будут пользоваться сторонние разработчики. Да и отлаживать проще.

          А так — полностью согласен. NSNotification очень удобны, особенно если сообщения нужно отправлять асинхронно.
          +2
          Может пригодится (если кто еще не видел): Cocoa Design Patterns
            0
            Либо я ничего не понимаю, либо это ппц.
            >>Теперь всякому объекту, которому необходимо знать об успешном прохождении уровня, достаточно реализовать протокол GameStateObserver и подписаться на оповещение об успешном завершении. Соотвествующий код будет выглядеть примерно так:

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

            То есть каждая фигнюшка которая просто хочет узнать об изменении GameState создает в памяти экземпляр класса GameState? У вас получится 40 разных экземпляров GameState?

            А объект GameState должен хранить в себе ссылки на все фигнюшки которые на него подписались?

            @interface GameState : NSObject {

            NSMutableSet *observerCollection;
            }


            Тут пахнет кривизной либо кода, либо Objective C.
            В Java и Java кастартах типа AS3 такие проблемы решаются классом Event.
              0
              Не так. Просто в том классе, в котором вам нужно принимать оповещение, вы должны реализовать протокол.
                0
                А зачем тогда это?
                GameState *gameState = [[GameState alloc] init];
                это не реализация протокола, это создание экземпляра класса
                  0
                  Это единый экземпляр. Просто нужно было объявить переменную, вот и втавил строчку кода. Как видно далее, этот объект регистрирует двух наблюдателей.
                    0
                    На практике при использовании NSNotificationCenter вызывается метод
                    — (void)addObserver:(id)observer selector:(SEL)aSelector name:(NSString *)aName object:(id)anObject;

                    На объекте observer вызовется aSelector
                  0
                  Объект создается один раз и хранит ссылки на всех подписчиков. NSNotificationCenter совершенно так же хранит ссылки на всех подписчиков, помня еще и нужный метод (селектор).

                  В java.util есть класс Observable и интерфейс Observer. Они реализуют паттерн напрямую.
                  0
                  Пользуюсь такой вот реализацией этого паттерна: MulticastDelegate.
                  Из преимуществ по сравнению с NSNotificationCenter —
                  1. автоматическая отписка observer-а при его удалении из памяти ( если забыли отписатся от оповещений в методе dealloc — не беда, отписка произойдет автоматически )
                  2. Поддержка произвольной сигнатуры метода оповещения, то есть можно нотифицировать так —

                  [ self.observers gameState: self didUpdateScore: self.score ];
                    0
                    Классно рассказано, все просто и понятно!
                    Паттерны переодически повторяю, но все равно что-то забывается.

                    Может напишите цикл статей про каждый паттерн из GoF с такими примерами?
                    Можно и без iOS. Для новичков (и не только) материал ценнейший будет!
                      0
                      Суть вы уловили. Про все паттерны не обещаю, но цикл будет!
                      +1
                      В рамках раскрытия темы observer в контексте iOS можно было бы еще вспомнить про паттерн delegate и определить между ними принципиальную разницу.
                        0
                        Когда буду писать про delegate, обязательно сравним! Кстати, GoF не выделяют его в паттерн.
                          0
                          Поддерживаю, прочитал статью и думал зачем же нужен обзервер, если есть делегейт. Хотя если множество объектов и им надо отослать события, тогда наверное лучше обзервер.

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

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