Добрый день, хабрачитатели. Спешу поделиться с вами опытом, недавно мной полученным.

Как вы, наверное, знаете — создание более менее внятных и серьезных приложений не может обойтись без грамотного проектирования. Одними из основных задач современного программирования — являются контроль над сложностью, требования создания гибких и расширяемых, изменяемых приложений. Из этого вытекают концепции ортогонального программирования, максимального уменьшения связности между классами, использования наиболее подходящих архитектурных решений (алсо грамотные подходы создания архитектуры проекта, подходы к проектированию классов). За многие человекочасы и человекодни мирового опыта всех разработчиков — были выработаны наиболее естественные и удачные подходы, названные паттернами проектирования… А подходы к проектированию классов — могут в некоторой степени изменяться, в зависимости от используемого языка программирования и требуемых свойств объекта. Описываемый сегодня мной паттерн является одним из моих самых любимых (и вообще достаточно значимый), а именно встречайте:… "Observer" (по-русски — Наблюдатель). Исходя из последних двух предложений — вытекает название этой статьи.
Наиболее полное и детальное описание паттерна Наблюдатель вы можете получить в известной книге «Банды четырех» — «Приемы объектно-ориентированного проектирования. Паттерны проектирования»
Еще есть неплохая шпаргалка по паттернам
Все паттерны делятся на 3 вида
— Поведенческие
— Порождающие
— Структурные
Observer является поведенческим паттерном.
Классическая реализация выглядит следующим образом, но как обычно, возможны некоторые отклонения от стандартной реализации

Наблюдатель позволяет снизить количество зависимостей в проекте, уменьшить связность, увеличить независимость объектов друг от друга (уменьшить знание одного объекта о другом, принцип инкапсуляции), и предлагает подход к решению некоторой группы задач. Касательно моего текущего проекта — у меня возникла следующая проблема:
Имелся контроллер представления для создания нового заказа (NewOrderViewController) в иерархии Navigation Controller-a, и от него шли переходы к другим представлениям (для выбора тарифа, для выбора перевозчика, для выбора маршрута, выбора даты заказа и выбора дополнительных сервисов). Ранее я вызывал пересчет цены заказа на viewWillAppear в NewOrderViewController, но это было не лучшее решение, потому-что требовалось отослать сетевой запрос, и пользователь мог некоторое время видеть индикатор ожидания (например). И вообще было бы логичнее совершать перерасчет цены заказа после изменения одного из упомянутых ранее параметров заказа. Можно было использовать бы делегирование (либо хранить слабые ссылки на NewOrderViewController), и вызывать в соответствующих местах метод перерасчета цены. Но этот подход чреват усложнением и некоторыми неудобствами. Был выбран более подходящий способ — создать наблюдателя, который будет отслеживать изменения моделей, вызывать у класса PriceCalculator-a метод перерасчета, который в свою очередь сообщал NewOrderViewController о результатах расчета цены/ моменте начала расчета цены с использованием делегирования.
Теперь нужно поговорить о том, как сконструировать наблюдателя. Эта абстракция должна быть максимально проста в использовании, максимально естественна и логична.
Во-первых нам нужно либо самостоятельно реализовывать одну из технологий наблюдения, либо воспользоваться какой-либо уже имеющейся.
— (если вручную) Сконструировать такую технологию можно с помощью создания отдельного потока выполнения и ран-лупа (цикла) с детектированием изменений соответствующих объектов, за которыми мы планируем вести наблюдение
— (если использовать уже что-либо готовое) Есть только 2 решения в стандартных фреймворках под iOS, способных удовлетворить решению подобной задачи
а) NSNotificationCenter (использование механизма уведомлений)
б) KVO (Key-value observing) (наблюдение за изменениями свойств классов)
У подхода с NSNotification-ами есть существенный недостаток — для этого пришлось бы перегружать сеттеры требуемых свойств, и создавать NSNotification c помощью
Наиболее существенный плюс KVO — минимальное влияние на наблюдаемый класс, также возможности конфигурирования наблюдаемости (observing options), относительная простота.
Имеется и довольно существенный недостаток — серьезное потребление производительности (в случае повсеместного использования), но в моем случае я решил с этим примириться
Таким образом, выбор пал на KVO
Некоторые полезные статьи про KVO:
Официальная документация (англоязычная), наиболее полная
два на английском
три хабровская
Для использования KVO вы должны понимать так-же основные принципы Key-value coding (кодирования ключ-значение)
KVO предоставляет методы добавления и исключения наблюдателя
И основной метод для регистрации изменения над наблюдаемыми свойствами
Так-же плюсами являются возможность выбирать NSKeyValueObservingOptions
— NSKeyValueObservingOptionNew — получает в NSDictionary новое значение (вызывается, когда значение изменяется)
— NSKeyValueObservingOptionOld — получает в NSDictionary старое значение (перед изменением)
— NSKeyValueObservingOptionInitial — метод обработки так-же срабатывает сразу же после назначения наблюдателя
— NSKeyValueObservingOptionPrior — обработчик срабатывает дважды (и до изменений, и после) (не уверен)
Опции аддитивны, можно выбирать сразу несколько, используя побитовое или
Еще один плюс — возможность отслеживать свойство не только текущего объекта, а и вложенных (все-таки keyPath)
К сожалению, я вынужден был потереть листинги кода!
Изначально была мысль создать базовый класс, реализующий наблюдателя, но было решено, что это нереентабельно. Поэтому все наблюдатели банально унаследовал от NSObject-ов. Так как наблюдатель должен реализовать отношение один-ко-многим, то был придуман механизм подписчиков. Каждый класс, который нуждается в оповещении об каких-либо изменениях — подписывается на наблюдателя и реализует соответствующий метод из протокола.
Каждый подписчик должен поддерживать протокол (для AddressPathObserver это —

Почему в этом есть нужда?
Как вы, наверное, знаете — создание более менее внятных и серьезных приложений не может обойтись без грамотного проектирования. Одними из основных задач современного программирования — являются контроль над сложностью, требования создания гибких и расширяемых, изменяемых приложений. Из этого вытекают концепции ортогонального программирования, максимального уменьшения связности между классами, использования наиболее подходящих архитектурных решений (алсо грамотные подходы создания архитектуры проекта, подходы к проектированию классов). За многие человекочасы и человекодни мирового опыта всех разработчиков — были выработаны наиболее естественные и удачные подходы, названные паттернами проектирования… А подходы к проектированию классов — могут в некоторой степени изменяться, в зависимости от используемого языка программирования и требуемых свойств объекта. Описываемый сегодня мной паттерн является одним из моих самых любимых (и вообще достаточно значимый), а именно встречайте:… "Observer" (по-русски — Наблюдатель). Исходя из последних двух предложений — вытекает название этой статьи.
Наиболее полное и детальное описание паттерна Наблюдатель вы можете получить в известной книге «Банды четырех» — «Приемы объектно-ориентированного проектирования. Паттерны проектирования»
Еще есть неплохая шпаргалка по паттернам
Все паттерны делятся на 3 вида
— Поведенческие
— Порождающие
— Структурные
Observer является поведенческим паттерном.
Классическая реализация выглядит следующим образом, но как обычно, возможны некоторые отклонения от стандартной реализации

Что это за «Наблюдатель», имеющиеся технологии
Наблюдатель позволяет снизить количество зависимостей в проекте, уменьшить связность, увеличить независимость объектов друг от друга (уменьшить знание одного объекта о другом, принцип инкапсуляции), и предлагает подход к решению некоторой группы задач. Касательно моего текущего проекта — у меня возникла следующая проблема:
Имелся контроллер представления для создания нового заказа (NewOrderViewController) в иерархии Navigation Controller-a, и от него шли переходы к другим представлениям (для выбора тарифа, для выбора перевозчика, для выбора маршрута, выбора даты заказа и выбора дополнительных сервисов). Ранее я вызывал пересчет цены заказа на viewWillAppear в NewOrderViewController, но это было не лучшее решение, потому-что требовалось отослать сетевой запрос, и пользователь мог некоторое время видеть индикатор ожидания (например). И вообще было бы логичнее совершать перерасчет цены заказа после изменения одного из упомянутых ранее параметров заказа. Можно было использовать бы делегирование (либо хранить слабые ссылки на NewOrderViewController), и вызывать в соответствующих местах метод перерасчета цены. Но этот подход чреват усложнением и некоторыми неудобствами. Был выбран более подходящий способ — создать наблюдателя, который будет отслеживать изменения моделей, вызывать у класса PriceCalculator-a метод перерасчета, который в свою очередь сообщал NewOrderViewController о результатах расчета цены/ моменте начала расчета цены с использованием делегирования.
Теперь нужно поговорить о том, как сконструировать наблюдателя. Эта абстракция должна быть максимально проста в использовании, максимально естественна и логична.
Во-первых нам нужно либо самостоятельно реализовывать одну из технологий наблюдения, либо воспользоваться какой-либо уже имеющейся.
— (если вручную) Сконструировать такую технологию можно с помощью создания отдельного потока выполнения и ран-лупа (цикла) с детектированием изменений соответствующих объектов, за которыми мы планируем вести наблюдение
— (если использовать уже что-либо готовое) Есть только 2 решения в стандартных фреймворках под iOS, способных удовлетворить решению подобной задачи
а) NSNotificationCenter (использование механизма уведомлений)
б) KVO (Key-value observing) (наблюдение за изменениями свойств классов)
У подхода с NSNotification-ами есть существенный недостаток — для этого пришлось бы перегружать сеттеры требуемых свойств, и создавать NSNotification c помощью
- postNotification:
, а в некоторых местах и явно указыватьНаиболее существенный плюс KVO — минимальное влияние на наблюдаемый класс, также возможности конфигурирования наблюдаемости (observing options), относительная простота.
Имеется и довольно существенный недостаток — серьезное потребление производительности (в случае повсеместного использования), но в моем случае я решил с этим примириться
Таким образом, выбор пал на KVO
Key-value Observing
Некоторые полезные статьи про KVO:
Официальная документация (англоязычная), наиболее полная
два на английском
три хабровская
Для использования KVO вы должны понимать так-же основные принципы Key-value coding (кодирования ключ-значение)
KVO предоставляет методы добавления и исключения наблюдателя
- (void)addObserver:(NSObject *)anObserver
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context;
- (void)removeObserver:(NSObject *)anObserver
forKeyPath:(NSString *)keyPath;
- (void)removeObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
context:(void *)context;
И основной метод для регистрации изменения над наблюдаемыми свойствами
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context;
Так-же плюсами являются возможность выбирать NSKeyValueObservingOptions
— NSKeyValueObservingOptionNew — получает в NSDictionary новое значение (вызывается, когда значение изменяется)
— NSKeyValueObservingOptionOld — получает в NSDictionary старое значение (перед изменением)
— NSKeyValueObservingOptionInitial — метод обработки так-же срабатывает сразу же после назначения наблюдателя
— NSKeyValueObservingOptionPrior — обработчик срабатывает дважды (и до изменений, и после) (не уверен)
Опции аддитивны, можно выбирать сразу несколько, используя побитовое или
Еще один плюс — возможность отслеживать свойство не только текущего объекта, а и вложенных (все-таки keyPath)
Текущая реализация
К сожалению, я вынужден был потереть листинги кода!
Изначально была мысль создать базовый класс, реализующий наблюдателя, но было решено, что это нереентабельно. Поэтому все наблюдатели банально унаследовал от NSObject-ов. Так как наблюдатель должен реализовать отношение один-ко-многим, то был придуман механизм подписчиков. Каждый класс, который нуждается в оповещении об каких-либо изменениях — подписывается на наблюдателя и реализует соответствующий метод из протокола.
Каждый подписчик должен поддерживать протокол (для AddressPathObserver это —
, для OrderObserver - , например :
соответственно нужно реализовать методы позволяющие добавлять/удалять подписчиков, а так-же структуру данных для хранения подписчиков
Выбор структуры данных - важный нюанс! Можно использовать массив, но массив - это упорядоченная коллекция, а в нашем случае не имеет смысла в очередности подписчиков (кто-то получает первым, кто-то позже), так как это и так происходит в довольно короткий промежуток времени. Таким образом, мне подходила неупорядоченная коллекция NSSet, но к сожалению и она не удовлетворяла всем требованиям. Потому-что множество хранит сильные ссылки. Если в такое множество запихнуть контроллер - то он не высвободит вовремя память, хотя будет уже неиспользуемым, из-за того, что единственная ссылка будет храниться в подписчиках, и будут отправлены лишние сообщения вхолостую. Конечно, такое может произойти только, если забыть отписаться, но лучше перестраховаться. Все забывают еще о двух полезных классах - NSMapTable и NSHashTable, которые предоставляют более гибкие возможности управлений памятью. NSHashTable - аналог NSSet, но позволяющий хранить свои объекты в виде слабых (weak) ссылок.
Так-же можно сделать метод класса, или свойство (если устанавливать) для получения множества ключей наблюдения. (в данном случае - метод класса). Конечно такая статичность имеет свои недостатки, и связывание на этапе исполнения чаще лучше, чем на этапе компиляции. Кстати, каждое из этих значений - либо задефайненый ключ, либо константа, которую можно определить в хедере. Есть и лучший способ определять ключи свойств , например :
KVO имеет один недостаток - если попытаться отписаться от наблюдения еще не наблюдаемого свойства - возникнет эксепшен. Для того, чтобы бороться с этим - был написан свой сеттер для наблюдаемого объекта.
Логично реализовать также методы включения/выключения наблюдения
Метод защищенного обновления наблюдения. Основная причина в том, что все-таки это нестабильная технология. Стоило бы в некоторых других местах тоже добавить такие блоки, но уже подзабыл.
Конкретно в текущей задаче была нужда именно в синглтон-объекте, который мог бы быть доступен из любого места приложения (глобальный, пускай это и не слишком хорошо)
Вот сама обработка, срабатывающая в момент модификации наблюдаемого объекта. Происходит несколько вещей - выбирается соответствующий селектор протокола (в зависимости от изменившегося свойства), и выбирается объект, который будет передан параметром. Далее прогоняются все подписчики в цикле, опрашиваются на реализацию метода с данным селектором, и делается запрос на запуск метода по переданному селектору. Все очень легко и просто ;)
Еще один очень важный момент! Автоматическое срабатывание обработки модификации происходит, если используется сеттер свойства. Но если наблюдаемый объект изменяется внутренне, или например происходят изменения в массиве/словаре - то нужно явно указывать, что значение свойства меняется, например :
[self willChangeValueForKey:@"addressPath"];
[_addressPath addObject:newAddressPoint];
[self didChangeValueForKey:@"addressPath"];
Моя реализация далека от идеала, но мир в целом несовершенен, но от этого он становится таким уж плохим как некоторым кажется))
ссылка на гитхаб репозиторий