Pull to refresh

Пишем реализацию Observer-а над KVO на objective-c

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


Почему в этом есть нужда?

Как вы, наверное, знаете — создание более менее внятных и серьезных приложений не может обойтись без грамотного проектирования. Одними из основных задач современного программирования — являются контроль над сложностью, требования создания гибких и расширяемых, изменяемых приложений. Из этого вытекают концепции ортогонального программирования, максимального уменьшения связности между классами, использования наиболее подходящих архитектурных решений (алсо грамотные подходы создания архитектуры проекта, подходы к проектированию классов). За многие человекочасы и человекодни мирового опыта всех разработчиков — были выработаны наиболее естественные и удачные подходы, названные паттернами проектирования… А подходы к проектированию классов — могут в некоторой степени изменяться, в зависимости от используемого языка программирования и требуемых свойств объекта. Описываемый сегодня мной паттерн является одним из моих самых любимых (и вообще достаточно значимый), а именно встречайте:… "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"];


Моя реализация далека от идеала, но мир в целом несовершенен, но от этого он становится таким уж плохим как некоторым кажется))

ссылка на гитхаб репозиторий
Tags:
Hubs:
Total votes 9: ↑5 and ↓4+1
Comments13

Articles