Sledování změn objektů: kód modelu - MujMAC.cz - Apple, Mac OS X, Apple iPod

Odběr fotomagazínu

Fotografický magazín "iZIN IDIF" každý týden ve Vašem e-mailu.
Co nového ve světě fotografie!

 

Zadejte Vaši e-mailovou adresu:

Kamarád fotí rád?

Přihlas ho k odběru fotomagazínu!

 

Zadejte e-mailovou adresu kamaráda:

Seriály

Více seriálů



Software

Sledování změn objektů: kód modelu

4. dubna 2006, 00.00 | Dnes se soustředíme na to, co budeme jako aplikační programátoři rozhodně dělat nejčastěji (nebudeme-li užívat knihoven Core Data) – totiž psaní modelů: Co musíme udělat, aby objekt KVO podporoval?

Minule jsme si ukázali na několika řádcích zdrojového kódu, jak vlastně funguje systém KVO (Key-Value Observing, sledování změn objektů) – sestavili jsme vlastní jednoduchou třídu, jež si pomocí zprávy addObserver:self forKeyPath:options:context: vyžádala informace o tom, kdy se obsah objektu v modelu změnil, a jež na základě těchto informací, přijatých prostřednictvím standardní metody observeValueForKeyPath:ofObject:change:context:, mohla dále pracovat (my jsme jen po chvíli změnili text, začínal-li náhodou písmenkem 'a').

Vlastně tedy zhruba víme, jak bychom mohli psát objekty "view" (či spíše kontroléry, jež by stály mezi view a modelem), založené na mechanismu KVO. K tomu se ještě vrátíme mnohem podrobněji časem, až se budeme zabývat třetím z revolučních mechanismů Cocoa, systémem bindings; prozatím se ale soustředíme na to, co budeme jako aplikační programátoři rozhodně dělat nejčastěji (nebudeme-li užívat knihoven Core Data) – totiž psaní modelů.

Podpora KVO: jde to samo od sebe

Aby bylo možné modelový objekt používat v rámci bindings, musí podporovat KVC (Key-Value Coding, přístup k pojmenovaným atributům) – tím jsme se zabývali v několika minulých článcích – a musí podporovat KVO. A co musíme udělat, aby objekt KVO podporoval? Inu, podobně jako tomu bylo u KVC, vlastně nic! Základní podpora KVO, stejně jako KVC, je zajištěna plně automaticky službami frameworku a třídy NSObject.

Připomeňme si základní pravidla, jež bylo zapotřebí dodržet pro systém KVC: atributy objektu mohly být uloženy bez jakéhokoli programování přímo v jeho proměnných; podle potřeby pro ně můžeme – ale nemusíme – implementovat accessory, u nichž je zapotřebí, aby dodržely základní jmennou konvenci value a setValue: – třeba nějak takto:

@interface Simple:NSObject {
  NSString *firstname,*surname;
}
-(void)setFirstname:(NSString*)name;
@end
@implementation Simple
-(void)setFirstname:(NSString*)name {
  [firstname autorelease];
  firstname=[[name capitalizedString] retain];
}
@end

(Samozřejmě, že tato ukázková modelová třída je umělá; jistý smysl však má – lze si představit, že zatímco u křestního jména chceme automaticky nastavit prvé písmeno velké, příjmení chceme ponechat beze změny kvůli jménům typu "deVille".)

Instance takovéto třídy můžeme ihned používat jako modelové objekty s KVC i s KVO, a vše bude fungovat zcela korektně: kdykoli kdokoli změní hodnotu kteréhokoli z obou atributů – ať již prostřednictvím zpráv KVC, tedy např. [foo setValue:@"Doe" forKey:@"surname"] nebo přímo ([foo setFirstname:@"john"]), systém se postará o to, aby byly korektně odeslány notifikace KVO všem objektům, jež si je vyžádaly pomocí zprávy addObserver:self forKeyPath:options:context:. Ano, skutečně to funguje automaticky i v případě, že voláme přímo accessor; jak je to možné, to ve skutečnosti nechcete vědět – jde o nejčernější objektovou magii ☺

Na druhou stranu ovšem vše má své meze: samozřejmě, že notifikace KVO by se neposlaly v případě, že bychom přímo změnili obsah proměnné – kupříkladu v takovéto implementaci atributu fullName:

-(void)setFullName:(NSString*)name {
  NSArray *a=[name componentsSeparatedByString:@" "];
  [self setFirstname:[a objectAtIndex:0]];
  [surname autorelease];
  surname=[[a lastObject] retain];
}

(Povšimněte si použití zprávy lastObject namísto objectAtIndex:1 – díky tomu se vyhneme výjimce v případě, že zadaný text obsahuje jen jedno slovo.)

Jistěže, těm, kdo se registrovali pro notifikace na změny obsahu atributu firstname, by notifikace přišla (neboť voláme set-accessor setFirstname:, a volání accessorů, jak víme, je díky černé magii s KVO kompatibilní automaticky); notifikaci by však nedostali ti, kdo se registrovali pro informace o změnách atributu surname: jeho obsah měníme přímo, takže odpovídající notifikace by se neodeslala.

Tento problém samozřejmě můžeme vyřešit tím, že použijeme "accessor" – tedy přístup prostřednictvím KVC – namísto přímého přístupu k proměnným (ostatně to je i o něco designově čistší):

-(void)setFullName:(NSString*)name {
  NSArray *a=[name componentsSeparatedByString:@" "];
  [self setFirstname:[a objectAtIndex:0]];
  [self setValue:[a lastObject] forKey:@"surname"];
}

Odvozené atributy

Můžeme-li ovšem atribut fullName nastavit, není ani nejmenší důvod, proč bychom neměli mít k dispozici i jeho zobrazení – implementované nejspíše nějak takto:

-(NSString*)fullName {
  return [NSString stringWithFormat:@"%@ %@",firstname,surname];
}

To ovšem přináší velkou obtíž ve chvíli, kdy atribut fullName připojíme k nějakému prvku grafického uživatelského rozhraní prostřednictvím KVO: neexistuje způsob, jak by mohl systém KVO automaticky rozeznat, že změna hodnot atributů firstname a surname vede i ke změně hodnoty odvozeného atributu fullName, takže je zapotřebí po změně libovolného z nich odeslat notifikaci i pro něj!

Návrháři systému Cocoa to ovšem dobře věděli, a proto není-li možné, aby to systém KVO věděl automaticky, máme k dispozici pohodlný způsob, jak mu to můžeme "říci" programově: libovolné třídě můžeme poslat standardní zprávu setKeys:triggerChangeNotificationsForDependentKey:, v níž určíme, na základě kterých atributů se hodnota odvozeného atributu generuje. Systém KVO pak zcela automaticky pro jakoukoli instanci takové třídy vygeneruje notifikace KVO i pro atribut odvozený.

Vzhledem k tomu, že se jedná o zprávu, již je zapotřebí poslat třídě dříve, než se začne jakkoli jinak používat, je k tomu nejvhodnější standardní třídní metoda initialize. O té jsme si zatím nepovídali; je to ale jednoduché – runtime Objective C prostě pošle každé třídě zprávu initialize předtím, než jí pošle jakoukoli zprávu jinou (speciálně tedy i zprávu alloc, tj. dříve, než vytvoří její prvou instanci). Stačí tedy do implementace třídy Simple přidat následující kód:

+(void)initialize {
  [self setKeys:
    [NSArray arrayWithObjects:@"firstname",@"surname",nil]
    triggerChangeNotificationsForDependentKey:@"fullName"];
}

a vše bude zcela automaticky fungovat jak má: změníme-li přímo hodnotu atributu fullName, notifikace pro atributy firstname a surname se pošlou díky tomu, že v implementaci metody setFullName: používáme accessory; naopak měníme-li atributy firstname a surname, notifikace pro atribut fullName se posílá díky globálnímu nastavení na úrovni třídy, zajištěnému při inicializaci zprávou setKeys:triggerChangeNotificationsForDependentKey:.

Mimochodem... jak už možná zkušenější programátory mezi čtenáři napadlo, pokud bychom z nějaké příčiny trvali na původní implementaci metody setFullName:, jež přistupovala přímo k proměnné surname, samozřejmě bychom mohli do metody initialize doplnit ještě příkaz [self setKeys:[NSArray arrayWithObject:@"fullName"] triggerChangeNotificationsForDependentKey:@"surname"] a vše by fungovalo správně. Systém KVO negeneruje notifikace na základě nastavení touto metodou transitivně – nehrozí tedy, že by na základě těchto dvou požadavků došlo k zacyklení. Přesto to ale není ani zdaleka ideální, a příště si ukážeme, jak to udělat lépe.

Odbočka pro pokročilé...

Ačkoli se to přímo netýká systému KVO, pro zkušenější programátory stojí za to se explicitně zmínit o jedné věci související spíše s accessory jako takovými, a trochu také se systémem KVC, jenž vlastně v jistém smyslu accessory supluje.

Ukázková implementace zjevně předpokládá, že objekty této modelové třídy budou (téměř) výhradně používány prostřednictvím KVC a ne přímo z programu – proto jsme neimplementovali triviální accessory

// nejsou třeba, užíváme-li důsledně KVC:
-(NSString*)firstname {
  return firstname;
}
-(NSString*)surname {
  return surname;
}
-(void)setSurname:(NSString*)name {
  [surname autorelease];
  surname=[name copy]; //*
}
@end

a ponechali jsme na systému KVC, aby si podle potřeby "sáhl" přímo na proměnné objektu. To je samozřejmě naprosto v pořádku, a ve většině případů je to moudré – čím méně kódu píšeme, tím menší je pravděpodobnost, že někde uděláme nějakou chybu. Jenže:

Neimplementujeme-li setXxx u atributů (na rozdíl od relací), lehce riskujeme!

Připomeňme nejprve rozdíl mezi atributem a relací 1:1, jak jsme si je definovali v základním popisu KVC: zatímco relace obsahuje odkaz na sdílený objekt, atribut obsahuje privátní nesdílenou kopii objektu. Implicitní "accessory" systému KVC samozřejmě nemohou "vědět", co je atribut a co je relace, a proto se chovají "relačně" – jinými slovy, používají na řádku označeném "//*" zprávu retain namísto zprávy copy.

V naprosté většině případů je to lhostejné; pokud ale náhodou narazíme na kód, který náš atribut nastaví na hodnotu měnitelného objektu a pak obsah tohoto objektu změní, náš atribut se změní také (protože – neimplementovali-li jsme accessor – ve skutečnosti není atributem, nýbrž relací 1:1). To ilustruje následující testovací kód; pokud vám není problém zcela zřejmý, vyzkoušejte si jej!

@interface Test:NSObject {
  NSString *ok,*bad;
}
@end
@implementation Test
-(void)setOk:(NSString*)s {
  [ok autorelease];
  ok=[s copy];
}
@end

int main() {
  [[NSAutoreleasePool alloc] init];
  Test *t=[[[Test alloc] init] autorelease];
  NSMutableString *ms=[NSMutableString stringWithString:@"ok"];
  [t setValue:ms forKey:@"ok"];
  [t setValue:ms forKey:@"bad"];
  NSLog(@"okay: %@ %@",[t valueForKey:@"ok"],[t valueForKey:@"bad"]);
  [ms appendString:@"_BAD!!!"];
  NSLog(@"bad: %@ %@",[t valueForKey:@"ok"],[t valueForKey:@"bad"]);
  return 0;
}

Obsah seriálu (více o seriálu):

Tématické zařazení:

 » Rubriky  » Informace  

 » Rubriky  » Agregator  

 » Rubriky  » Software  

 

 

 

Nejčtenější články
Nejlépe hodnocené články
Apple kurzy

 

Přihlášení k mému účtu

Uživatelské jméno:

Heslo: