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

от автора

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

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

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

OrderModifyDelegate

@protocol OrderModifyDelegate <NSObject>  @optional  -(void)orderPriceWasChanged:(NSString*)newPrice; -(void)orderTariffWasChanged:(Tariff*)newTariff; -(void)orderCarrierWasChanged:(Carrier*)newCarrier; -(void)orderSelectedServicesWasModified:(NSDictionary*)selectedServices; -(void)orderServiceTextWasChanged:(NSString*)newServiceText; -(void)orderDateWasChanged:(NSString*)newDate;  @end 

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

Методы менеджмента подписчиков

-(void)addSubscriber:(id<OrderModifyDelegate)modificationOrderSubscriber; -(void)removeSubscriber:(id<OrderModifyDelegate)modificationOrderSubscriber; 

-(void)addSubscriber:(id<OrderModifyDelegate>)modificationOrderSubscriber{          if(! modificationOrderSubscriber){         @throw [NSException exceptionWithName:@"nullObjectException" reason:@"try to adding nil subscriber" userInfo:nil];     }     if(! [modificationOrderSubscriber conformsToProtocol:@protocol(OrderModifyDelegate) ]){         @throw [NSException exceptionWithName:@"protocolException" reason:@"try to adding subscriber, when not conformed current observation protocol" userInfo:nil];     }          [self.hashTableSubscribers addObject:modificationOrderSubscriber]; }  -(void)removeSubscriber:(id<OrderModifyDelegate>)modificationOrderSubscriber{          if(! [self.hashTableSubscribers containsObject:modificationOrderSubscriber]){         @throw [NSException exceptionWithName:@"nullObjectException" reason:@"try to removing subscriber, that is not in hash table" userInfo:nil];     }     [self.hashTableSubscribers removeObject:modificationOrderSubscriber]; } 

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

@property (strong, nonatomic, readonly) NSHashTable *hashTableSubscribers; 

-(instancetype)init{          if(self = [super init]){         _hashTableSubscribers = [NSHashTable hashTableWithOptions:NSPointerFunctionsWeakMemory];     }     return self; } 

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

NSString* const ORDER_PRICE_KEY_PATH = NSStringFromSelector(@selector(price)); 

+(NSSet*)observableKeys; 

+(NSSet*)observableKeys{     return [NSSet setWithArray:@[ORDER_PRICE_KEY_PATH,                                  ORDER_TARIFF_KEY_PATH,                                  ORDER_CARRIER_KEY_PATH,                                  ORDER_SERVICES_KEY_PATH,                                  ORDER_SERVICE_TEXT_KEY_PATH,                                  ORDER_DATE_KEY_PATH]]; } 

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

@property (weak, nonatomic) OrderInfo *observedOrder; 

-(void)setObservedOrder:(OrderInfo *)observedOrder{          if(_observedOrder){         [self endObserving];     }      _observedOrder=observedOrder;     [self startObserving]; } 

Логично реализовать также методы включения/выключения наблюдения

изменение состояния наблюдения

-(void)startObserving; -(void)endObserving; 

-(void)startObserving{          NSKeyValueObservingOptions observingOptions = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial;     for(NSString *keyPath in [[self class] observableKeys]){         [_observedOrder addObserver:self forKeyPath:keyPath options:observingOptions context:nil];     } }  -(void)endObserving{          for(NSString *keyPath in [[self class] observableKeys]){         [_observedOrder removeObserver:self forKeyPath:keyPath];     } } 

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

Обновление наблюдаемого объекта

-(void)refreshObserving; 

-(void)refreshObserving{          @try {         self.observedOrder = [self observedOrderInfo];     }     @catch (NSException *exception) {         //отсылать нон-краш репорт на гугл-аналитикс         NSString *reportDescription = [NSString stringWithFormat:@"New Address Path - %@ , Subscribers - %@", [self observedOrderInfo], [self.hashTableSubscribers allObjects]];         [[GAIClient sharedInstance] sendReportNonFailExceptionWithDescription:reportDescription];         [[TaxseeLogs sharedLogs] taxseeLogWithType:TaxseeLogsUndefined withFormat:@"OBSERVING ORDER EXCEPTION REPORT SENDED !!!"];     } }  - (OrderInfo*)observedOrderInfo{          AppDelegate *appDelegate = (AppDelegate*)[UIApplication sharedApplication].delegate;     return appDelegate.orderInfo; } 

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

Синглтон-конструктор

+(instancetype)sharedOrderObserver{          static OrderObserver *sharedOrderObserver;     static dispatch_once_t onceToken;     dispatch_once(&onceToken, ^{         sharedOrderObserver = [OrderObserver new];         [sharedOrderObserver refreshObserving];     });     return sharedOrderObserver; } 

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

Срабатывание изменения

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{          [self handleModificationPropertyWithKey:keyPath]; }  -(void)handleModificationPropertyWithKey:(id)propertyKey{          NSString *currentKey = (NSString*)propertyKey;     id modifiedObject = nil;     SEL modifySelector = NULL;     if([currentKey isEqualToString:ORDER_PRICE_KEY_PATH]){                  modifySelector = @selector(orderPriceWasChanged:);         modifiedObject = self.observedOrder.price;     }else if([currentKey isEqualToString:ORDER_TARIFF_KEY_PATH]){                  modifySelector = @selector(orderTariffWasChanged:);         modifiedObject = self.observedOrder.tariff;     }else if([currentKey isEqualToString:ORDER_CARRIER_KEY_PATH]){                  modifySelector = @selector(orderCarrierWasChanged:);         modifiedObject = self.observedOrder.carrier;     }else if([currentKey isEqualToString:ORDER_SERVICES_KEY_PATH]){                  modifySelector = @selector(orderSelectedServicesWasModified:);         modifiedObject = self.observedOrder.selectedServices;     }else if([currentKey isEqualToString:ORDER_SERVICE_TEXT_KEY_PATH]){                  modifySelector = @selector(orderServiceTextWasChanged:);         modifiedObject = self.observedOrder.serviceText;     }else if([currentKey isEqualToString:ORDER_DATE_KEY_PATH]){                  modifySelector = @selector(orderDateWasChanged:);         modifiedObject = self.observedOrder.date;     }          if(modifySelector == NULL){         return;     }          for(id<OrderModifyDelegate> currentSubscriber in [self.hashTableSubscribers allObjects]){                  if([currentSubscriber respondsToSelector:modifySelector]){             [currentSubscriber performSelector:modifySelector withObject:modifiedObject];         }     } } 

Еще один очень важный момент! Автоматическое срабатывание обработки модификации происходит, если используется сеттер свойства. Но если наблюдаемый объект изменяется внутренне, или например происходят изменения в массиве/словаре — то нужно явно указывать, что значение свойства меняется, например:

[self willChangeValueForKey:@"addressPath"]; [_addressPath addObject:newAddressPoint]; [self didChangeValueForKey:@"addressPath"]; 

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

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

ссылка на оригинал статьи http://habrahabr.ru/post/264823/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *