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