Nastal čas na kakao - Podtřídy, delegáti, vkládání, jak se to rýmuje? - 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ů



Informace

Nastal čas na kakao - Podtřídy, delegáti, vkládání, jak se to rýmuje?

8. července 2004, 00.00 | Inu, třeba: na podtřídy bacha, delegát ten fachá, vkládání je řacha, tak se to rýmuje. Zatímco dědičnost se v Cocoa oproti jiným objektovým prostředím nepoužívá tak často, s výhodou se zde využívají alternativní techniky, mezi něž patří práce s delegáty či vkládání objektů.

Inu, třeba: na podtřídy bacha, delegát ten fachá, vkládání je řacha, tak se to rýmuje. Zatímco dědičnost se v Cocoa oproti jiným objektovým prostředím nepoužívá tak často, s výhodou se zde využívají alternativní techniky, mezi něž patří práce s delegáty či vkládání objektů.

V řadě více či méně objektových prostředí je zvykem dědičnosti využívat takřka na vše možné i nemožné: chceme třeba omezit velikost okna nejvýše na 640x480? Dobrá, vytvoříme si vlastní podtřídu standardní systémové třídy Window, v níž reimplementujeme metodu resize patřičným způsobem, a pro dané okno ji použijeme. Chceme snad tlačítko, jež ukončí aplikaci? Vytvoříme vlastní podtřídu standardní systémové třídy Button, reimplementujeme v ní metodu clicked tak, že aplikaci ukončí, a pro dané tlačítko ji použijeme. Co třeba potřebujeme-li prioritní frontu objektů? Inu, vytvoříme podtřídu standardní třídy Array, a přidáme patřičné služby...

V Cocoa takto obvykle nepostupujeme; to proto, že takovéto přehnané nadužívání dědičnosti není úplně ideální objektový design, a v praxi přináší řadu problémů: nezřídka vede k tomu, že bychom potřebovali vícenásobnou dědičnost (jež je v C++ problematická a jinde není k dispozici vůbec), často je také jeho důsledkem kombinace služeb z logicky různých bloků aplikace v jediném zdrojovém souboru. V důsledku pak omezuje reusabilitu a komplikuje další úpravy kódu.

Dnes se proto blíže podíváme na základní techniky, jež Cocoa nabízí (a prostřednictvím svých standardních tříd také využívá) vedle a namísto tvorby nových podtříd.

Podtřídy

Začít je vhodné tím, že samozřejmě v Cocoa zcela běžně podtřídy vytváříme! Součástí standardních knihoven je dokonce řada tříd, jež jsou přímo určeny k tomu, abychom v konkrétních aplikacích používali jejich konkrétní podtřídy, a samy o sobě téměř nemají smysl: NSView, NSImageRep, NSDocument...

Důležité však je to, že – právě na rozdíl od jiných objektových prostředí – Cocoa často nabízí i jiné prostředky pro dosažení téhož cíle, a velmi často jsou tyto prostředky daleko šikovnější, znamenají menší námahu programátora (a menší pravděpodobnost chyb). Obecně proto v Coca platí následující pravidlo:

Pokud nepracujeme přímo s třídou, jež je navržena pro dědění, měli bychom nejprve zvážit, zda pro vyřešení daného problému nalezneme vhodnou cestu bez vytváření nových tříd. Teprve pokud tomu tak není, měli bychom začít uvažovat o podtřídách; ne dříve.

Podívejme se znovu na příklady z úvodu:

  • chceme omezit velikost okna nejvýše na 640x480? Nejjednodušší je použít zcela standardní okno, a přidělit mu delegáta – objekt, jehož se okno "zeptá", má-li změnit velikost daným způsobem;
  • chceme tlačítko, jež ukončí aplikaci? Nejjednodušší je použít standardní tlačítko, definovat jeho akci jako zprávu terminate:, a přidělit mu jako cíl objekt aplikace;
  • potřebujeme prioritní frontu objektů? Nejjednodušší je použít docela standardní pole, a odpovídající služby doplnit prostřednictvím kategorie;
  • případně můžeme vytvořit nový objekt jako dědice třídy NSObject a pole vložit do něj. To je výhodnější v případě, že požadujeme i další atributy – dejme tomu, že by naše prioritní fronta měla mít jméno.

Delegace

Nejjednodušší – a také patrně nejčastěji používaný – vzorec, který dokáže nahradit řadu případů nešikovného dědění, je delegace. Princip je krajně prostý: namísto toho, aby se třída sama o všechno starala prostřednictvím svých metod (jež její dědicové mohou reimplementovat), instance třídy obsahuje odkaz na spolupracující objekt, tzv. delegáta, s nímž nejrůznější akce prostřednictvím odpovídajících zpráv "konsultuje".

Zásadní a klíčovou výhodou tohoto přístupu oproti dědění je to, že můžeme funkčně odlišné bloky kódu skutečně rozdělit do různých tříd. Objektovým designem podle vzorce MVC (Model, View, Controller) se budeme zabývat až později, až se budeme učit jak v Cocoa sestavovat grafické uživatelské rozhraní; i bez větší teorie však snad je zřejmé, že zatímco kód který určuje jak vypadá a jak funguje ovladač pro změnu velikosti logicky patří do objektu "okno", kód který určuje jak se to které konkrétní okno může zmenšovat či zvětšovat tam vůbec nemá co dělat: ten patří do ovladače, který okno řídí, ale rozhodně ne do okna samotného.

Kromě toho využití delegace obvykle i zjednoduší kód aplikace: jen výjimečně delegáta připravujeme jako nový, samostatný objekt; častěji jde o objekt, který slouží zároveň pro více logicky souvisejících věcí – může např. jako delegát řídit okno pro zobrazení seznamu položek, a zároveň jako zdroj dat předávat tyto položky tabulce, jež je uvnitř okna zobrazuje; kromě toho se může ještě starat o aktivaci/deaktivaci tlačítek, jež nad položkami pracují...

Základní mechanismus delegace je prostý, a sami jej ve vlastních třídách můžeme s výhodou využívat: instance prostě má k dispozici proměnnou, obsahující odkaz na delegáta, metody pro přístup k této proměnné, jež se standardně jmenují delegate a setDelegate – a před důležitými operacemi a/nebo po jejich provedení se delegáta zeptá (či jej informuje).

Standardní technika je využít tzv. neformálního protokolu pro deklaraci zpráv, jež jsou delegátovi posílány: nejde o nic jiného, než o rozhraní kategorie pro třídu NSObject. Díky němu překladač zprávy zná a vyhneme se "warningům" při jejich posílání:

// OCJumper.h
#import <Foundation/Foundation.h>
// deklarace zpráv, posílaných delegátovi:
@class OCJumper;
@interface NSObject (OCJumperDelegate)
-(BOOL)jumperShouldJump:(OCJumper*)jumper; // dotaz, zda má být akce provedena
-(void)jumperWillJump:(OCJumper*)jumper; // informace o budoucí akci
-(void)jumperDidJump:(OCJumper*)jumper; // informace o provedené akci
@end
// deklarace vlastní třídy:
@interface OCJumper:... {
  id delegate;
  ...
}
-delegate;
-(void)setDelegate:del;
...
-(void)jump;
...
@end

Povšimněme si standardního tvoření jmen zpráv posílaných delegátům: jméno by vždy mělo začínat jménem třídy bez případného prefixu. Následuje should, will či did, jež specifikuje, zda jde o dotaz má-li být akce provedena, o (již nevratnou) informaci o budoucí akci, nebo o informaci o tom, že akce byla ukončena. Pak následuje jméno akce; součástí zprávy samozřejmě musí být argument, který obsahuje konkrétní instanci, jež s delegátem komunikuje (protože jediný delegát může sloužit více různým objektům zároveň). Zprávy obecně samozřejmě mohou mít více argumentů (v tom případě by instance byla hned za jménem třídy: "-(void)jumper:(OCJumper*)jumper will...").

V implementaci stojí za zvláštní zmínku snad jen využití standardní metody respondsToSelector: (zděděné od třídy NSObject) pro ověření, zda delegát té které zprávě skutečně rozumí. To je důležité proto, abychom při implementaci delegáta mohli připravit pouze ty metody, jež potřebujeme, a nemuseli se zdržovat implementací ostatních, jež nás v tu chvíli nezajímají:

// OCJumper.m
#import "OCJumper.h"
@implementation OCJumper
-delegate {
  return delegate;
}
-(void)setDelegate:del {
  delegate=del; // delegát se nikdy "neretainuje"!
}
...
-(void)performJump { // vlastní akce: není v rozhraní, jde o privátní metodu
  ...
}
-(void)jump {
  // pokud delegát rozumí zprávě jumperShouldJump: a odpoví NO...
  if ([delegate respondsToSelector:@selector(jumperShouldJump:)] &&
     ![delegate jumperShouldJump:self]) return; // ... neskáčeme
  // pokud delegát rozumí zprávě jumperWillJump:...
  if ([delegate respondsToSelector:@selector(jumperWillJump:)])
    [delegate jumperWillJump:self]; // ...pošleme mu ji
  [self performJump]; // provedeme vlastní akci
  // pokud delegát rozumí zprávě jumperDidJump:...
  if ([delegate respondsToSelector:@selector(jumperDidJump:)])
    [delegate jumperDidJump:self]; // ...pošleme mu ji
}
...
@end

Důvody proč se delegátu neposílají zprávy retain a autorelease jsou prosté: v praxi je velmi často delegátem objekt, který aplikaci jako celek řídí, a naopak tedy sám podle potřeby vytváří a ruší – jinými slovy tedy "retainuje" a uvolňuje – objekty, jimž slouží jako delegát. Tyto objekty samy tedy delegáta "retainovat" nesmějí, neboť by to vedlo k zacyklení a ani delegát, ani objekt by již nikdy nebyly uvolněny.

Na závěr stojí za to se stručně zmínit o tom, že v moderním Cocoa je klasické využití delegátů, jež jsme si zde popsali, do jisté míry nahrazováno využitím tzv. notifikací, jež je o něco flexibilnější (ale také náročnější). My se notifikacím budeme věnovat později; prozatím je jen vhodné si zapamatovat, že narazíme-li náhodou na problém, při němž bychom potřebovali pro nějakou třídu definovat "více delegátů najednou", je načase podívat se na služby, jež nabízí třída NSNotification

Mechanismus akce/cíl

Ačkoli mechanismus akce/cíl (action/target) se převážně používá u objektů grafického uživatelského rozhraní, stojí za to si jej alespoň ve stručnosti ukázat hned; podrobně se mu budeme věnovat časem, až se soustředíme na GUI.

Jde o určitou alternativu delegace: podobně, jako objekt může udržovat odkaz na delegáta, jejž informuje o významných operacích a/nebo se jej ptá má-li operace provést a jak, může objekt udržovat odkaz také na cíl (target): to je objekt, který bude informován o provedení nějaké zcela zásadní operace. Zcela jednoznačné to bývá právě u objektů grafického uživatelského rozhraní – takto zásadní operací pro tlačítko je jeho stisknutí, pro nabídku výběr některé z jejích položek, pro textové pole ukončení jeho editace a podobně.

Mechanismus akce/cíl je oproti delegaci flexibilnější v tom, že i samotná zpráva, již objekt svému cíli posílá, je proměnná: není tedy napevno určena prostřednictvím neformálního protokolu, jako tomu je u zpráv delegáta; namísto toho instance obsahuje proměnnou typu SEL, a použije standardní metody zděděné od třídy NSObject pro odeslání zprávy – velmi přibližně takto:

// Button.h -- princip
...
@interface Button:... {
  SEL action;
  id target;
  ...
}
...
@end
// Button.m -- princip
...
@implementation Button
...
-(void)performClick { // tlačítko bylo stisknuto
  [target performSelector:action withObject:target];
}
...
@end

Samozřejmě, výhody oproti využití dědičnosti jsou obrovské: nejenže – stejně jako u delegace – nemusíme "míchat" kód pro řízení aplikace s kódem samotného tlačítka; navíc vlastně nemusíme psát vůbec nic "navíc". Pokud je požadovaná služba již někde k dispozici – např. ukončení programu, jež existuje jako hotová metoda terminate: standardní třídy NSApplication – prostě vezmeme tlačítko, a jen nastavíme vhodně jeho target a action (v praxi v Cocoa je na to k dispozici velmi pohodlný grafický prostředek, InterfaceBuilder, takže dokonce ani to nemusíme dělat programově).

Příště se podíváme blíž na pokročilejší techniky – na rozšíření služeb prostřednictvím kategorie, a na vkládání objektů.

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

Tématické zařazení:

 » Rubriky  » Informace  

 » Rubriky  » Agregator  

 » Rubriky  » Začínáme s  

 

 

 

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

 

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

Uživatelské jméno:

Heslo: