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


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

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

соответственно нужно реализовать методы позволяющие добавлять/удалять подписчиков, а так-же структуру данных для хранения подписчиков

Выбор структуры данных — важный нюанс! Можно использовать массив, но массив — это упорядоченная коллекция, а в нашем случае не имеет смысла в очередности подписчиков (кто-то получает первым, кто-то позже), так как это и так происходит в довольно короткий промежуток времени. Таким образом, мне подходила неупорядоченная коллекция NSSet, но к сожалению и она не удовлетворяла всем требованиям. Потому-что множество хранит сильные ссылки. Если в такое множество запихнуть контроллер — то он не высвободит вовремя память, хотя будет уже неиспользуемым, из-за того, что единственная ссылка будет храниться в подписчиках, и будут отправлены лишние сообщения вхолостую. Конечно, такое может произойти только, если забыть отписаться, но лучше перестраховаться. Все забывают еще о двух полезных классах — NSMapTable и NSHashTable, которые предоставляют более гибкие возможности управлений памятью. NSHashTable — аналог NSSet, но позволяющий хранить свои объекты в виде слабых (weak) ссылок.

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

KVO имеет один недостаток — если попытаться отписаться от наблюдения еще не наблюдаемого свойства — возникнет эксепшен. Для того, чтобы бороться с этим — был написан свой сеттер для наблюдаемого объекта.
Логично реализовать также методы включения/выключения наблюдения

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

Конкретно в текущей задаче была нужда именно в синглтон-объекте, который мог бы быть доступен из любого места приложения (глобальный, пускай это и не слишком хорошо)

Вот сама обработка, срабатывающая в момент модификации наблюдаемого объекта. Происходит несколько вещей — выбирается соответствующий селектор протокола (в зависимости от изменившегося свойства), и выбирается объект, который будет передан параметром. Далее прогоняются все подписчики в цикле, опрашиваются на реализацию метода с данным селектором, и делается запрос на запуск метода по переданному селектору. Все очень легко и просто ;)

Еще один очень важный момент! Автоматическое срабатывание обработки модификации происходит, если используется сеттер свойства. Но если наблюдаемый объект изменяется внутренне, или например происходят изменения в массиве/словаре — то нужно явно указывать, что значение свойства меняется, например:
[self willChangeValueForKey:@"addressPath"];
[_addressPath addObject:newAddressPoint];
[self didChangeValueForKey:@"addressPath"];


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

ссылка на гитхаб репозиторий

Комментарии (13)


  1. Krypt
    15.08.2015 17:17

    > Потому-что множество хранит сильные ссылки.
    Если очень хочется использовать NSSet/NSDictionary/NSArray — можно обернуть объект в NSValue:
    [NSValue valueWithNonretainedObject:]


    1. HUktoCode
      15.08.2015 19:50

      это же лишний раз заворачивать во враппер нужно)


  1. PHmaster
    15.08.2015 17:22
    +3

    Что-то я не совсем уловил суть. KVO — это и есть уже готовая реализация паттерна Observer. Зачем писать реализацию над реализацией? Чтобы вы могли реализация пока реализация реализация?


    1. HUktoCode
      15.08.2015 19:49
      -2

      это комментарий немного в стиле «зачем писать на высокоуровневых языках, если можно писать на асме». А если больше по теме — то мы должны стараться использовать более точные абстракции, соответствующие задаче. Вот например, мы же вместо NSURLConnection — стремимся использовать именно AFNetworking (потому-что он более удобен), или например, RestKit для работы с RESTful сервисами, потому-что эти библиотеки представляют нам более удобные интерфейсы для нашей задачи. В некоторых случаях, мы конечно, теряем управляемость, и тратим лишнее время (если реализуем собственные решения), но в дальнейшем мы можем иметь выигрыш (особенно в крупных проектах). Потому-что в дальнейшем низкоуровневый код будет сложно модифицировать и понимать.


      1. PHmaster
        16.08.2015 04:43

        С одной стороны, вроде бы мотивация ясна. С другой стороны, для меня это всё равно выглядит, как бесполезное усложнение. Абстракция ради абстракции. Штатное kvo, к примеру, легко использовать для биндингов к UI. И не только (пользуюсь им постоянно, для различных целей, нареканий никаких нет). Ваше решение, видимо, только Вам одному понятно, для чего можно использовать. Хотя, главное, конечно, чтобы Вы сами ощущали полезность Вашей абстракции и прирост продуктивности от ее использования в Вашем проекте.


  1. i_user
    15.08.2015 18:07

    Программист на фортранеджаве на любом языке может писать на фортране?

    А если по теме — то если у нас сущность подписывается на изменение одного поля в модели — то зачем тянуть наблюдение за остальными? (пусть даже на уровне протокола)
    А если на изменение нескольких — то не проще ли (и не чище ли) подписываться на изменение объекта модели в целом?


    1. HUktoCode
      15.08.2015 19:56

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

      Если подписываться на изменение модели — это требует написания лишних строчек willChangeValueForKey/didChangeValueForKey в местах, где происходят изменения объектов. Хотя, кажется, там был какой-то автоматический механизм. В любом случае это тоже неплохая идея, но сделал уже именно так, и оно неплохо сразу заработало


      1. i_user
        15.08.2015 21:16
        +1

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

        Могу порекомендовать подробно поизучать ReactiveCocoa и прочие порты Rx — оно, конечно, оверкилл с точки зрения производительности — но при осознанном использовнии невероятно упрощает поддержку кода бизнес-слоя.


        1. Krypt
          15.08.2015 23:38

          Например модель, отвечающая за авторизацию:
          Её могут слушать как и контроллер чего-нибудь интерфейсного, так и другая модель.
          Или же состояние сети. Аналогично, его могут слушать как и модели, чтобы сделать запрос к сети, когда появится доступ, так и контроллер, чтобы показать пользователю надпись, «извините, нет соединения с интернет»

          Правда, я реализую это вещи по другому, но как пример — сгодятся.
          Интернет — это чаще всего Reachibility, использующий NSNotificationCenter, а авторизация (как и прочие модели) — «множественные» делегаты.


          1. i_user
            15.08.2015 23:41

            Вот, вы приводите как раз превосходные примеры атомарных моделей. В которых нам интересно изменение всего состояния авторизации, либо же сети, а не одного из 5 полей, что значительно упрощает работу по организации подписки — как через Notification Center, так и через KVO или же множественное делегирование, реализованное, как-нибудь иначе.

            У меня вызывает вопросы как раз необходимость дробления модели до ее полей.


            1. Krypt
              16.08.2015 00:03
              +1

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

              В принципе, основная проблема изкоробочного KVO в obj-c, на мой взгляд, — это «божественный метод», который уведомляется о всех изменениях в слушателей: observeValueForKeyPath. Эта проблема — причина которой я и не использую KVO.

              Насколько я понял автора — это попытка решать эту проблему. Объект, который перенаправляет уведомления KVO в соответствующие им селекторы. В принципе, хуже не стало. На счёт лучше… Тоже не уверен. Вообще, можно реализовать это гораздо прозрачнее.


            1. PHmaster
              16.08.2015 04:47

              Насколько я помню, NSObjectController как-то связан за слежением за объектом в целом. Хотя, могу и ошибаться.


  1. ajjnix
    15.08.2015 18:11
    +1

    Чем меньше в системе изменяемых моделей, тем спокойнее работать