Здравствуйте. После недавнего перехода с objectivе c на java обнаружил отсутствие в java столь хороших вещей как свойств и KVO(Key-Value Observing). В данной статье расказано о том зачем в принципе усложнять доступ к внутренним переменным объекта, как это реализованно objective c и какую пользу с точки зрения слежения за состоянием объектов мы получаем, при использовании свойств и KVO.
Часть 1 — property
Свойства в objective c это грубо публичные переменные класса с модифицированными геттерами/сеттерами. Для того, чтобы лучше понять зачем это нужно посмотрим на пример. Допустим у нас есть класс company хранящий в себе число работников данной компании (people):
class company {
int people;
}
Мимолетным взором замечаем потенциальный источник ошибок — число людей в компании объявлено как int включающий также и отрицательные значения. В данном случае проблему можно устранить заменой типа переменной на беззнаковую, но что делать если мы хотим еще больше сузить диапазон допустимых значений. Скажем в соответствии с ТЗ число работников в компании не может быть больше тысячи.
Типичное решение данной проблемы в языках лишенных встроенной поддержки свойств это создание пары акцессор/мутатор (accessor/mutator):
class company {
private int people;
void setPeople(int people){
if(people>0)
this.people=people;
else
this.people=0;
}
int getPeople(){
return this.people;
}
}
Мутатор позволяет изменять значение переменной с учетом наложенных ограничений, а наличие акцессора диктуется необходимостью скрыть внутреннюю переменную. Иначе любой мог бы обратиться к переменной напрямую и записать туда некорректный результат.
Казалась бы проблема решена, но стоит отметить один факт — использование пар акцессора/мутатора сильно затрудняет чтение кода. Сравните код увеличения числа рабочих на единицу:
company_A.setPeople(company_A.getPeople()+1);
и
company_A.people++;
Очевидно что во втором случае код более читаем, да и заставлять программиста писать для каждой переменной пары акцессор/мутатор не хорошо. Язык objective c позволяет избежать написания этого скучного кода с помощью использования свойств. Типичное описание класса с использованием свойств выглядит так:
// company.h
@class company : NSObject {
@property int people;
@end
А в реализации класса достаточно дописать одну строчку, а остальное компилятор сделает сам.
// company.m
@implementation company
@synthesize people;
@end
После этого мы можем обращаться к нашей переменной через привычную точку, но при использовании свойств возможно указать ряд дополнительных параметров как то:
- Права доступа к переменной — readwrite/ readonly при обращении извне.
- Многопоточный доступ — atomic/nonatomic( синхронизировать чтение/запись между потоками или нет)
- Каким образом происходит присвоение — copy/weak/strong. Указание опции copy заставляет при присвоении снимать копию с исходного значения, а не просто приравнивать указатели. Опции strong и weak управляют временем жизни переменной (strong — предотвращает уничтожение переменной сборщиком мусора, weak — нет)
Как видно число опций достаточно велико для удовлетворения типичных потребностей при написании кода. В случае же наличия ограничений на присылаемые значения необходимо вручную переопределить метод set. Также можно вручную переопределить метод get (что в ряде случае позволяет избавиться от лишних переменных). Только стоит помнить, что в objective c функция акцессора пишется без префикса get, а synthesize позволяет указать как обращаться к переменной внутри класса. Пример кода.
//company.h
@interface company : NSObject
@property (atomic, readwrite )int people;
@property (atomic, readonly )BOOL isBigCompany;
@end
//company.m
@implementation company
@synthesize people= _people
-int people{
return _people;
}
- void setPeople(int people){
if(people>0)
_people=people;
else
_people=-10;
}
- BOOL isBigCompany{
return _people>1000;
}
@end
//использование
company *company_A=[[company alloc] init];
company_A.people=2000;
if(company_A.isBigCompany){
company_A.people=-1;
}
// результат company_A.people=-10
Выгода от использования свойств налицо. Мы можем не только избавиться от написания лишнего кода, но и создавать псевдопеременные, не раскрывая как они по-настоящему устроены.
Помимо упрощения синтаксиса использование свойств позволяет использовать еще несколько возможностей языка, как то — обращение к переменной через ее название.
[company_A valueForKey:@"people"];
Или даже записать туда новое значение. А учитывая возможность получения списка всех свойств прямо вовремя выполнения программы это открывает большие возможности. Замечание по моему личному мнению подобной возможностью лучше не пользоваться ибо это скорее грязных хак, чем типичный способ обращения к переменным. Подробнее про objective c runtime можно прочитать например здесь.
Перед тем как рассказывать об KVO стоит упомянуть и об уведомлениях в уведомлениях(notification)
Часть 2 — notification
Objective c позволяет быстро и с минимальными усилиями реализовать патерн наблюдатель. В двух словах патерн наблюдатель это шаблон проектирования, когда существует два объекта — наблюдатель и наблюдаемый. Наблюдатель интересуются событиями происходящими с наблюдаемым, а вот второй наблюдаемый флегматично течет по течению изредка посылая уведомления о происходящих с ним событиях. Наблюдаемый даже не интересуется, а слышит ли его кто-нибудь, но продолжает рассылать уведомления. Подобный подход позволяет реализовывать обратную связь к происходящим с наблюдаемым изменениям.
Objective c имеет очень продвинутый механизм работы с уведомлениями, так чтобы зарегистрировать наблюдателя достаточно одной строчки кода:
[[NSNotificationCenter defaultCenter] addObserver:object selector:@selector(observeNotification:) name:nameNotification object:nil];
Где object — объект который будет наблюдателем, сложная конструкция @selector(observeNotification:)
указывает какой метод надо вызвать у наблюдателя (в данном случае observeNotification:) и nameNotification — название события за которым мы будем наблюдать. В последнем параметре можно указать от какого объекта мы ожидаем получить уведомление, или же просто получать все уведомления с заданным именем, что удобно если заранее неизвестно кто именно пошлет уведомление.
В классе наблюдателя необходимо реализовать указанный выше метод (observeNotification:), а в объекте который посылает уведомления достаточно написать:
[[NSNotificationCenter defaultCenter] postNotificationName:nameNotification object:self];
И на этом необходимый для простейшей реализации патерна наблюдатель код заканчивается. Как видно нам не пришлось писать какого либо кода, который бы хранил связи между наблюдателем и наблюдаемым, но пока имелась необходимость в изменение кода наблюдаемого. А вот KVO позволяет избежать излишнего вмешательства в чужой код.
Часть 3 — KVO
KVO (key-value observing) — еще одна реализация патерна наблюдатель, даже более интересная чем предыдущая. Если в прошлом варианте наблюдатель следил за абстрактными событиями, зная только имя, то в KVO область применения несколько сужается. В этом случае наблюдатель четко знает объект за которым наблюдает и за каким именно свойством (property) у объекта он следит. Когда значение этого свойства меняется, наблюдателю приходит уведомление и он соответствующим образом реагируют.
По сравнению со многими другими языками реализация KVO в objective c радуют довольно простым синтаксисом. Так в коде наблюдателя достаточно написать:
[company_a addObserver:self forKeyPath:@"people" options:NSKeyValueObservingOptionNew context:nil];
и каждый раз когда в company_a будет изменяться значение переменной people наблюдатель будет уведомляться с помощью вызова метода observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
и надо лишь реализовать код, который будет реагировать на уведомление.
Вы наверное хотели спросить, а что надо дописать в коде класса объекта за которым ведется наблюдение ( в данном случае company) для реализации подобной замечательной возможности — так вот если использовать свойства с автоматически синтезироваными директивой @ synthesize методами get/set, то ничего дописывать не надо. В противном случае код изменяющий внутреннюю переменную необходимо окружить двумя вспомогательными функциями(willChangeValueForKey: и didChangeValueForKey:). Так ранее использованный пример стоит доработаь следующим образом:
//company.h
@interface company : NSObject
@property (atomic, readwrite )int people;
@property (atomic, readonly )BOOL isBigCompany;
@end
//company.m
@implementation company
@synthesize people= _people
-(int) people{
return _people;
}
+(BOOL)isBigCompanyWithPeople:(int)people{
return people>1000;
}
- (void) setPeople(int people){
BOOL isBigCompanyOld=self.isBigCompany;
BOOL isBigCompanyNew= [company isBigCompanyWithPeople:people];
if(isBigCompanyNew!=isBigCompanyOld)
[self willChangeValueForKey:@"isBigCompany"];
if(people>0)
_people=people;
else
_people=0;
if(isBigCompanyNew!=isBigCompanyOld)
[self didChangeValueForKey:@"isBigCompany"];
}
- (BOOL) isBigCompany{
return [company isBigCompanyWithPeople:_people];
}
@end
Теперь можно подписаться только на слежение за свойством isBigCompany и не следить за точным значением переменной people. Уведомления будут приходить только тогда, когда свойство isBigCompany будет меняться.
А учитывая, что в подавляющем большинстве случае программисты не забывают дописать необходимые строки мы получаем очень мощный инструмент для слежения за другими объектами. Перечислю достоинства этого подхода
- Минимализм кода. ( достаточно написать всего лишь несколько строчек, чтобы полностью реализовать паттерн наблюдатель)
- Возможность слежения за любыми свойствами(property) любых классов как написанными нами, так и чужими. Фактически внешние переменные всегда оформляются через свойства, что позволяет с легкостью следить любыми изменениями.
- Повторяюсь возможность слежениям за любыми свойствами любых классов будь то state у NSOperation или frame у UIView. KVO позволяет следить за изменением параметров любого класса
- И еще раз — не важно кто писал класс, есть ли его исходный код или нет. У вас будет возможность следить за изменениями и это просто фантастическая вещь, которая на порядок упрощает написание кода реакций на события.
Фактически выше три раза упомянуто одно и то же достоинство, но это только потому, что аналогов столь мощного механизма в других популярных языках нет. В случае если вы хотите отреагировать каким либо образом на изменение одного из параметров, вам необходимо или влезать внутрь и переписывать код объекта (что в большинстве случаев просто не возможно) или же изобретать другие еще более сложные обходные пути. KVO же позволяет решить эту проблему быстро и с минимальным количество труда, что делает KVO одной из главной фишек objective C.
Автор: intet