Objective-C: notifiche e delegati

In Objective-C abbiamo due modi molto utilizzati per ricevere ed inviare messaggi tra classi: le notifiche e i delegati. La differenza tra i due, oltre che essere a livello di implementazione, dipende sostanzialmente da “quanti” – oggetti – possono ricevere un messaggio. Prima di tutto lasciatemi mostrare come nasce il concetto di delegato.

Delegare

Immaginiamo di avere la nostra solita classe A (il nostro ricevitore in questo esempio) la quale istanzia un oggetto di class B (il nostro trasmettitore). La classe A vuole essere avvertita quando una determinata operazione, svolta dalla classe B, è terminata. Una rozza implementazione di questo concetto, del tutto legittima e funzionante, potrebbe essere: la Classe A passa un suo puntatore alla Classe B, cosicché quando quest’ultima vuole “avvertire” la Classe A, non deve far altro che invocare un metodo utilizzando il puntatore a lei passato.

Il codice della Classe B, quindi, potrebbe essere:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// ----------------------------------------------------------------------
// file ClassB.h

@class ClassA;
@interface ClassB : NSObject {
  ClassA *pointerClassA;
}
// Metto a disposizione una proprietà con cui la Classe A può
// passarmi il suo puntatore
@property(assign) ClassA *pointerClassA;

@end

// ----------------------------------------------------------------------
// file ClassB.m

#import "ClassB.h"
#import "ClassA.h"

@implementation ClassB

@synthesize pointerClassA;

- (void)operazione {
  // ... operazione
  // Invia alla classe A l'avviso di aver terminato
  [pointerClassA metodoDiAvviso];
}

@end

La Classe A sa che Classe B accetta un suo puntatore (Classe A e Classe B si conoscono, anzi… si devono conoscere in questo esempio), inoltre sa anche che quando la Classe B avrà terminato il suo compito invocherà un metodo chiamato metodoDiAvviso. Quindi, Classe A, non deve far altro che implementare tale metodo e passare a Classe B il suo puntatore. Tutto questo, tradotto in codice, è:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// ----------------------------------------------------------------------
// file ClassA.h

@class ClassB;
@interface ClassA : <NSObject> {
  ClassB *classB;
}

- (void)metodoDiAvviso;
- (void)metodoQualsiasi;

@end

// ----------------------------------------------------------------------
// file ClassA.m

#import "ClassA.h"
#import "ClassB.h"

@implementation ClassA

- (id)init {
  self = [super init];
  if(self) {
    classB = [[ClassB alloc] init];
    classB.pointerA = self; // Comunico alla Classe B il mio puntatore
  }
  return self;
}

- (void)metodoQualsiasi {
  // ...
  [classB operazione];
}

- (void)metodoDiAvviso {
  // Metodo invocato dalla Classe B
  NSLog(@"%s: la Classe B ha terminato e mi ha avvisato!", __FUNCTION__);
}

@end

Nonostante questa tecnica funzioni, dimostra tutti i suoi limiti se usata in contesti anche di piccole dimensioni. Vediamo quali sono le cose che non vanno:

  • Le classi devono conoscersi a vicenda: ognuna ha importato le definizioni dell’altra
  • La Classe A deve implementare e dichiarare il metodo ricevitore
  • La Classe B assume che esista una Classe A, cioè che il puntatore pointerA sia valorizzato
  • Il tutto è realizzato esplicitando i nomi delle classi in gioco
  • Se Classe B volesse inviare un messaggio alla Classe C si dovrebbe: o cambiare il codice o duplicare il giro

Fortunatamente esiste un metodo molto più elegante, chiaro e semplice da usare per affrontare queste situazioni. Ne avevamo già parlato in Come creare un proprio protocollo con delegato. Objective-C permette di definire un protocollo di comunicazione tramite il quale una o più classi possono scambiarsi informazioni.

Nota: potreste trovare che i protocolli di Objective-C sono indicati come un modo per ereditare l’interfaccia (non l’implementazione) di altre classi. Questo è vero, anche se ci sono alcune sostanziali differenze in questo caso rispetto a quello che accade con altri linguaggi.

Nel nostro caso il protocollo va definito all’interno della Classe B, e adottato dalla Classe A. In pratica una classe definisce qual’è il suo protocollo di comunicazione. Le classi che intendo ricevere i messaggi da quest’ultime, ne adottano i protocolli.
Rivediamo dunque l’esempio precedente e riscriviamolo utilizzando protocolli e delegati:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// ----------------------------------------------------------------------
// file ClassB.h

@protocol ClassBDelegate;
@interface ClassB : NSObject {
  id<ClassBDelegate> delegate;
}
// Delegato
@property(assign) id<ClassBDelegate> delegate;

@end

@protocol ClassBDelegate <NSObject>
@optional
// Messaggio inviato al delegato al termine dell'operazione
- (void)metodoDiAvviso;
@end

// ----------------------------------------------------------------------
// file ClassB.m

#import "ClassB.h"

@implementation ClassB

@synthesize delegate;

- (void)operazione {
  // ... operazione
  // Se esiste un delegato e tale delegato è conforme al protocollo
  // ClassBDelegate invia l'avviso di aver terminato
  if(delegate && [delegate respondsToSelector:@selector(metodoDiAvviso)]) {
    [delegate metodoDiAvviso];
  }
}
@end

Nel nuovo codice possiamo già notare un notevole passo avanti: non c’è alcuna indicazione relativa alla Classe A. Quest’ultima, dal canto suo, dovrà invece adottare esplicitamente il protocollo messo a disposizione dalla Classe B se vuole esserne un delegato e quindi riceverne le notifiche:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// ----------------------------------------------------------------------
// file ClassA.h

#import "ClassB.h" // Adotto il protocollo ClassBDelegate

@interface ClassA : <NSObject, ClassBDelegate> {
  ClassB *classB;
}

- (void)metodoQualsiasi;

@end

// ----------------------------------------------------------------------
// file ClassA.m

#import "ClassA.h"

@implementation ClassA

- (id)init {
  self = [super init];
  if(self) {
    classB = [[ClassB alloc] init];
    // Comunico alla Classe B di essere io il suo delegato
    classB.delegate = self;
  }
  return self;
}

- (void)metodoQualsiasi {
  // ...
  [classB operazione];
}

// Conforme al protocollo adottato
- (void)metodoDiAvviso {
  // Metodo invocato dalla Classe B
  NSLog(@"%s: la Classe B ha terminato e mi ha avvisato!", __FUNCTION__);
}

@end

Questo procedimento è notevolmente più pulito e semplice da implementare. I vantaggi sono:

  • La classe che vuole delegare (Classe B) non deve conoscere i suoi delegati; gli basta che essi siano conformi al suo protocollo. In questo modo qualsiasi classe può ricevere i messaggi
  • La classe che vuole ricevere il messaggio (Classe A) adotta esplicitamente il/i protocollo/i, che può adottare in parte o nella loro totalità in base a come sono stati definiti

L’unico problema di questo approccio è legato al numero di delegati. Nel nostro esempio l’istanza della Classe B veniva creata all’intero della Classe A. In questo contesto risulta ovvio (ma non è detto) che sia Classe A ad essere il delegato. In una diversa variante Classe A potrebbe istanziare Classe C e fornire quest’ultima come delegato a Classe B (Classe C dovrà adottare il protocollo ClassBDelegate). Comunque sia, quello che risulta evidente è che ad una istanza è legato un solo delegato; Classe B non può avere due o più delegati, pena fornire n proprietà delegate.

Naming

Spendo sue parole sul naming, cioè su come nominare i protocolli e metodi di quest’ultimi. Sul nome del protocollo, se desiderate seguire le regole Apple, normalmente si usa appendere Delegate alla fine del nome della classe che definisce il protocollo. In altri casi, quando il protocollo non è legato a nessuna classe in particolare, si usa appendere la forma ...ing inglese, per non confondere il protocollo con una classe. Quindi, ad esempio:

1
2
3
4
NSLook sarà NSLooking, NSLook sembrerebbe una classe
ClasseB definirà il suo protocollo ClasseBDelegate
UIWebView definisce UIWebViewDelegate
...

Il metodo definito nel protocollo, invece, segue la regola che dovrebbe passare sempre il puntatore di chi ha inviato il messaggio, seguito dai parametri se ci sono. Ad esempio:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(int)row;
- (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename;

// Se non ci sono parametri
- (BOOL)applicationOpenUntitledFile:(NSApplication *)sender;

// Si usa "Did" oppure "Will" per notificare al delegato che qualcosa è
// successo oppure che qualcosa sta per accadere
- (void)browserDidScroll:(NSBrowser *)sender;
- (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)window;

// Il prefisso "should" viene invece usato per chiedere al delegato
// se si vuole che qualcosa accada
- (BOOL)windowShouldClose:(id)sender;

Notifiche

Quando un oggetto, ovvero l’istanza di una classe, può essere condivisa da più oggetti e si ha la necessità di avvertire tutti gli oggetti interessati, conviene utilizzare un metodo diverso per notificare i messaggi, le cosiddette notifiche.
Notifiche e protocolli/delegati possono tranquillamente convivere all’interno dello stesso codice; questo significa che se abbiamo già implementato un sistema protocollo/delegato, nulla vieta di aggiungere anche le notifiche. lasciatemi quindi mostrare l’esempio iniziale realizzato con il solo uso delle notifiche.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ----------------------------------------------------------------------
// file ClassB.h

@interface ClassB : NSObject {
}

@end

// ----------------------------------------------------------------------
// file ClassB.m

#import "ClassB.h"

@implementation ClassB

@synthesize pointerClassA;

- (void)operazione {
  // ... operazione
  // Invia la notifica a tutti coloro che si sono registrati
  [[NSNotificationCenter defaultCenter] postNotificationName:@"operazioneTerminata" ];
}

@end

Come si evince dal codice qui sopra, questa tecnica non nulla di aggiuntivo, a parte l’invio della notifica. Anche chi vuole ricevere la notifica dovrà fare davvero poco:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// ----------------------------------------------------------------------
// file ClassA.h

@interface ClassA : <NSObject> {
  ClassB *classB;
}

- (void)metodoQualsiasi;

@end

// ----------------------------------------------------------------------
// file ClassA.m

#import "ClassA.h"

@implementation ClassA

- (id)init {
  self = [super init];
  if(self) {
    classB = [[ClassB alloc] init];
    // Mi registro come interessato al messaggio "operazioneTerminata"
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(notificaOperazioneTerminata)
                                                 name:@"operazioneTerminata"
                                               object:nil];
  }
  return self;
}

- (void)metodoQualsiasi {
  // ...
  [classB operazione];
}

// Il nome di questo metodo lo decida la Classe A; può essere
// qualsiasi quindi
- (void)notificaOperazioneTerminata {
  // Metodo invocato dalla Classe B
  NSLog(@"%s: la Classe B ha terminato e mi ha avvisato!", __FUNCTION__);
}

- (void)dealloc {
  // Rimuovo me stesso dalla ricezione di notifiche riguardanti il
  // messaggio identificato con la stringa @"operazioneTerminata"
  [[NSNotificationCenter defaultCenter] removeObserver:self
                                                  name:@"operazioneTerminata"
                                                object:nil];
  [super dealloc];
}

@end

Naming

Anche qui due parole sul naming delle notifiche, sempre in accordo con i suggerimenti Apple. Il nome della notifica, che è una NSString, viene normalmente costruita seguendo questo criterio:

1
[Name of associated class] + [Did | Will] + [UniquePartOfName] + Notification

Nell’esempio fatto sopra avremmo dovuto inserire nel file .h della Classe B:

1
2
3
4
5
6
7
8
9
10
// Nel file header della Classe B
extern NSString * const ClassBDidFinishedNotification;

// Nel file di implementazione della Classe B
NSString * const ClassBDidFinishedNotification = @"ClassBDidFinishedNotification";

// Il metodo nella Classe A, invece, è più libero, ma poteva essere scritto
// ad esempio appendendo "Notification" alla fine - questo comunque ha meno
// importanza
- (void)finishedNotification;

Parametri

Entrambe le tecniche sopra esposte permettono di inviare messaggi con numero arbitrario di parametri. Riprendiamo gli esempi di sopra e proviamo a passare due parametri. Nel caso del protocollo/delegato, avremmo potuto avere:

1
2
3
4
5
@protocol ClassBDelegate <NSObject>
@optional
// Messaggio inviato al delegato al termine dell'operazione
- (void)classB:(ClassB *)aClassB avvisoConID:(int)aID;
@end

Nell’implementazione si sarebbe dovuto scrivere:

1
2
3
4
5
6
7
8
- (void)operazione {
  // ... operazione
  // Se esiste un delegato e tale delegato è conforme al protocollo
  // ClassBDelegate invia l'avviso di aver terminato
  if(delegate && [delegate respondsToSelector:@selector(classB:avvisoConID:)]) {
    [delegate classB:self avvisoConID:57];
  }
}

Come vedete molto molto semplice.

Per le notifiche il discorso è simile, anche se qui si usa un unico parametro userInfo che normalmente (se non è nil) contiene un puntatore ad un NSDictionary con la collezione dei parametri. Al momento dell’invio della notifica avremmo avuto:

1
2
3
4
5
6
7
8
- (void)operazione {
  // ... operazione
  // Invia la notifica a tutti coloro che si sono registrati
  [[NSNotificationCenter defaultCenter] postNotificationName:@"operazioneTerminata"
                                      object:self
              userInfo:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:57]
                                                   forKey:@"avvisoConID"]];
}

Al momento della lettura della notifica:

1
2
3
4
5
6
7
- (void)notificaOperazioneTerminata:(NSNotification *)aNotification {
  // Metodo invocato dalla Classe B
  NSLog(@"%s: la Classe B ha terminato e mi ha avvisato!", __FUNCTION__);

  NSNumber *avvisoConID = [[aNotification userInfo] objectForKey:@"avvisoConID"];
  NSLog(@"%s: %d", __FUNCTION__, [avvisoConID intValue]);
}

In conclusione, quando usare delegati/protocolli e notifiche?

Come abbiamo visto entrambe le soluzioni sono semplici, potenti e facili da implementare. Se gli eventi all’interno dell’istanza della nostra classe possono interessare più oggetti, non abbiamo altra scelta che usare le notifiche. In tutti gli altri casi il delegati/protocolli o entrambe le soluzioni.
Normalmente consiglio di partire dall’implementazione di delegati e protocolli, in quanto se dovesse accadere che un evento è di interesse collettivo, può sempre essere inviato anche in notifica.

5 commenti a: “ ”

  1. 11 mar, 2011 Paul:

    Ottimo articolo!
    Chiaro, preciso e ricco di esempi!
    Blog molto interessante!
    Saluti

  2. 20 nov, 2011 iLeW:

    Non è che si potrebbe avere un esempio finale dell’utilizzo dei delegati? Qui ci si è limitati alla definizione, ma poi un esempio pratico di utilizzo come sarebbe (rifacendosi al codice dell’esempio)?

  3. 24 nov, 2011 Objective-C: addendum su notifiche e delegati - Undolog.com - Undolog.com:

    [...] alla domanda di ILeW con un articolo vero e proprio per spiegare meglio, allegando esempio, come funzionano delegati e [...]

  4. 02 feb, 2012 luigi:

    molto chiaro e semplice
    devo ammettere che anche scrivendo da un pà difficilmente uso delegati creati da me…ovviamente è una tecnica che mi sono promesso di utilizzare più spesso anche se forzandone la necessità
    complimenti

Lascia un commento

TAG XHTML PERMESSI: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> INSERIMENTO CODICE:
<pre></pre> // blocco generico
                   <code></code> // blocco generico
                   [cc_actionscript][/cc_actionscript] // Actionscript
                   [cc_actionscript3][/cc_actionscript3] // Actionscript 3
                   [cc_css][/cc_css] // CSS Style Sheet
                   [cc_html][/cc_html] // HTML
                   [cc_js][/cc_js] // Javascript
                   [cc_objc][/cc_objc] // Objective-C
                   [cc_php][/cc_objc] // PHP
                   [cc_sql][/cc_sql] // SQL