Implémentation d’un système de délégation multiple en Objective-C

written by netinfluence 24 août 2009
Implémentation d’un système de délégation multiple en Objective-C

1. Qu’est-ce que la délégation?

La délégation est un système présent dans plusieurs classes du framework Cocoa, sous Mac OS X (et donc également sur iPhone OS).
Ce système permet aux dévelopeurs d’applications Cocoa d’interagir en fonction d’événement précis liés aux fonctionnements intrinsèques des objets Cocoa.
Prenons par exemple l’objet NSWindow, qui comme son nom l’indique permet d’afficher et de contrôler une fenêtre.
Cet objet fenêtre possède un certain nombre de méthode, comme par exemple ‘close’, ou ‘open’, permettant respectivement d’ouvrir et de fermer le fenêtre en question.
Lors de la programmation d’une application Cocoa, il peut être fort utile d’être informé lors de l’ouverture ou de la fermeture d’une fenêtre, pour allouer ou libérer des ressources, terminer des tâches ou des threads, etc.
Le système de délégation du framework Cocoa permet ainsi d’attacher une instance d’un objet à un autre objet, le premier ayant la possibilité d’agir sur le second en fonction de ses différentes phases d’exécution.
La définition d’un objet délégué sur un autre objet se passe par convention par la méthode ‘setDelegate’, prenant en argument unique l’instance du délégué.
Par exemple, pour définir un objet de type ‘Foo’ comme délégué d’un objet NSWindow:

Foo * foo = [ [ Foo alloc ] init ];
NSWindow * window = [ [ NSWindow alloc ] initWithContentRect: NSMakeRect( 0, 0, 100, 100 ) styleMask: NSTitledWindowMask backing: NSBackingStoreBuffered defer: NO ];

[ window setDelegate: foo ];

Les deux premières lignes créent respectivement un objet de type ‘Foo’ (défini dans notre application), et un objet de type ‘NSWindow’ (du framework Cocoa).

La troisième ligne défini l’objet de type ‘Foo’ comme délégué de notre objet ‘NSWindow’.
A partir de ce moment, à supposer que l’on ferme notre objet fenêtre:
[ window close ];
L’objet délégué pourra être averti de cette opération, en implémentant tout simplement une méthode spécifique. Dans le cas précis, ‘windowWillClose’, dont voici le prototype:
- ( void )windowWillClose: ( NSNotification * )notification;
Celle-ci permettra au délégué, juste avant la fermeture de la fenêtre, d’effectuer un certain nombre d’opérations, liées au bon fonctionnement de l’application.

2. Comment fonctionne la délégation?

Voyons maintenant comment l’objet ‘NSWIndow’ implémente et utilise son objet délégué.
L’objet ‘NSWindow’ contient bien sûr une variable d’instance de type ‘id’, représentant l’objet délégué, généralement nommée delegate, ainsi qu’une méthode permettant d’obtenir l’objet délégué, et une seconde permettant de le définir.
Autrement dit:
@interface NSWindow: NSObject
{
    @protected

    id delegate;
}

- ( id )delegate;
- ( void )setDelegate: ( id )object;

@end

@implementation NSWindow

- ( id )delegate
{
    return delegate;
}

- ( void )setDelegate: ( id )object
{
    delegate = object;
}

@end
Ici, il est extrêmement important de préciser qu’un objet ne doit en aucun cas effectuer un ‘retain’ sur son objet délégué, car cela aurait comme effet un ‘memory leak’, autrement dit une zone de mémoire allouée à notre application qui ne serait jamais libérée.
Il est également à noter qu’en Objective-C 2.0, l’utilisation d’une propriété peut être utilisée dans l’interface, pour permettre un accès aisé à l’objet délégué:
@property( nonatomic, assign, readwrite ) delegate;
A partir de là, les getter/setter peuvent également être automatiquement délcarés dans l’implémentation:
@synthesize delegate;
Si l’on reprend maintenant l’exemple de la méthode ‘close’ de l’objet ‘NSWindow’:
- ( void )close
{
    // Do something...

    if( [ delegate respondsToSelector: @selector( windowWillClose ) ] ) {
        [ delegate windowWillClose ];
    }

    // Do something...
}
A un instant précis dans l’exécution de la méthode ‘close’, l’objet ‘NSWindow’ se demande si son objet délégué possède une méthode nommée ‘windowWillClose’.
Si c’est le cas, elle l’exécute, et poursuit sa propre exécution.
La méthode ‘close’ n’a nullement besoin de vérifier si un objet délégué a été défini auparavant, étant donné qu’en Objective-C, envoyer un message (appeler une méthode) sur ‘nil’ (un pointeur NULL sur un objet) est parfaitement valide.
L’objet délégué sera donc averti, à supposer qu’il implémente la méthode ‘windowWillClose’, de la fermeture de l’objet ‘NSWindow’.

3. Délégation et notification

Le framework Cocoa inclut également un système de notification, permettant là aussi à des objets d’être informé des stades d’exécution d’autres objets.
Dans notre exemple précédent, nous aurions donc aussi pu, pour être averti lors de la fermeture de la fenêtre, écrire le code suivant.
[ [ NSNotificationCenter defaultCenter ] addObserver: foo selector: @selector( myObserverMethod: ) name: NSWindowWillCloseNotification object: window ]:
Autrement dit, on déclare que la méthode ‘myObserverMethod’ de l’objet de type ‘Foo’ doit être lancée lors de l’événement ‘NSWindowWillCloseNotification’ de la fenêtre. Dans ce cas, voici le prototype de la méthode ‘myObserverMethod’:
- ( void )myObserverMethod: ( NSNotification * )notification;
On peut donc ici se demander pourquoi ces deux systèmes cohexistent.
Ce qu’il faut bien comprendre, c’est que le système de notification permet uniquement d’être averti de certains événements, alors que le système de délégation permet en plus de modifier le comportement de l’objet concerné.
Prenons le cas de la méthode ‘windowShouldClose’, pouvant être implémentée dans le délégué d’un objet ‘NSWindow’, et dont voici le prototype:
- ( BOOL )windowShouldClose: ( NSWindow * )window;
On remarque que cette méthode retourne une valeur booléenne.
Si notre objet délégué implémente cette méthode, et qu’on lance la méthode ‘close’ sur notre fenêtre, cette dernière ne se fermera effectivement que dans le cas où la méthode du délégué retourne la valeur ‘YES’. Dans le cas contraire, elle restera affichée sur l’écran.
Cela peut par exemple permettre de bloquer le processus de fermeture de la fenêtre, pour afficher un message d’alerte demandant à l’utilisateur s’il veut sauvegarder ses changements éventuels.
A ce moment là, c’est le délégué qui prend la responsabilité de savoir si et quand la fenêtre doit être fermée, ce qui serait évidemment impossible via le système de notification.
On voit donc ici bien la différence de logique entre la délégation et la notification, le premier pouvant agir sur l’objet concerné, au contraire du second.
Certains objets du framework Cocoa utilisent également leur délégué pour obtenir d’autre types d’informations, comme par exemple l’objet ‘NSBrowser’ (la vue en colonne du Finder) qui utilise son délégué pour connaître les éléments à afficher.

4. Chaîne de délégation

A ce niveau là, on peut remarquer une certaine limitation dans le système de délégation. Un objet ne peut avoir qu’un seul et unique délégué.
Prenons le code suivant:
[ window setDelegate: foo ];
[ window setDelegate: bar ];
Le délégué de l’objet ‘window’ sera donc ‘bar’, qui remplacera ‘foo’ à ce poste. L’objet ‘foo’, quand à lui, ne sera plus du tout en mesure de contrôler les différents aspects de la fenêtre.
Or il pourrait être fort utile, dans certains cas, de pouvoir placer plusieurs délégués sur un même objet. Nous allons donc implémenter un système permettant de chaîner les objet délégués.

5. Implémentation – MultipleDelegateObject

Nous allons tout d’abord créer un classe, qui servira de base aux classes voulant implémenter une chaîne de délégués:
/* MultipleDelegateObject.h */
@interface MultipleDelegateObject: NSObject
{
    @protected
    DelegateChain * delegate;
}

- ( void )addDelegate: ( id )object;
- ( void )removeDelegate: ( id )object;
- ( NSArray * )delegates;

@end;
Nous n’allons pas gérer la chaîne de délégué dans cette objet, mais plutôt créer une seconde classe, nommée ‘DelegateChain’, que nous étudierons après.
Notre classe contient donc une méthode permettant d’ajouter un délégué, une méthode permettant d’en enlever un, et une méthode permettant d’obtenir sous forme de tableau tous les objets délégués.
L’implémentation est la suivante:
/* MultipleDelegateObject.m */
@implementation

- ( id )init
{
    if( ( self = [ super init ] ) ) {
        delegate = [ [ DelegateChain alloc ] init ];
    }

    return self;
}

- ( void )dealloc
{
    [ delegate release ];
    [ super dealloc ];
}

- ( void )addDelegate: ( id )object
{
    [ delegate addDelegate: object ];
}

- ( void )removeDelegate: ( id )object
{
    [ delegate removeDelegate: object ];
}

- ( NSArray * )delegates
{
    return [ delegate delegates ];
}

@end
La méthode ‘init’ crée une nouvelle instance de la classe ‘DelegateChain’, et la stocke dans la variable d’instance ‘delegate’. La méthode ‘dealloc’ libère cette resource lors de la destruction de l’objet.
Les trois autres méthodes ne font que re-router les appels sur l’objet de type ‘DelegateChain’, qui gérera elle les délégués multiples.

6. Implémentation – DelegateChain

Regardons maintenant l’interface de la classe ‘DelegateChain’:
/* DelegateChain.h */
@interface DelegateChain: NSObject
{
    @protected

    id * delegates;
    NSUInteger numberOfDelegates;
    NSUInteger sizeOfDelegatesArray;
    NSMutableDictionary * hashs;
}

- ( void )addDelegate: ( id )object;
- ( void )removeDelegate: ( id )object;
- ( NSArray * )delegates;

@end
Comme précisé auparavant, il ne faut pas effectuer de ‘retain’ sur les objets délégués. Il nous est donc impossible d’utiliser un objet de type ‘NSMutableArray’ ou ‘NSMutableDictionary’ pour stocker les délégués, car ceci aurait pour effet un ‘retain’ automatique lors de l’ajout.
Un tableau de pointeur sur les délégués (le type ‘id’ est en fait un pointeur), alloué et ré-alloué si nécessaire avec les fonctions d’allocation de mémoire de la bibliothèque standard C, fera par contre très bien l’affaire. Il s’agit dans notre cas de la variable d’instance ‘delegates’.
Nous avons également une variable contenant le nombre de délégués actuellement affectés (‘numberOfDelegates’), et une autre (‘sizeOfDelegatesArray’) contenant la taille du tableau de pointeur précédemment cité, et dont nous verrons l’utilité plus tard.
La variable ‘hash’, quand à elle, servira à stocker les adresses mémoire des délégués, afin de trouver facilement leur position dans le tableau de pointeur.
Regardons maintenant, méthode par méthode, l’implémentation de la classe ‘DelegateChain’. En tout premier lieu, son initialisation:
- ( id )init
{
    if ( ( self = [ super init ] ) ) {

        hash = [ [ NSMutableDictionary dictionaryWithCapacity: 10 ] retain ];

        if( NULL = ( delegates = ( id * )calloc( 10, sizeof( id ) ) ) ) {

            // Error management...
        }
    }

    return self;
}
Nous créons le dictionnaire qui nous permettra de stocker les adresses mémoire, et nous réservons une zone dans la mémoire pour stocker les pointeurs vers nos objets délégués. Nous réservons une zone pouvant contenir 10 objets, afin d’éviter d’avoir à appeler les fonctions d’allocation à chaque fois qu’un délégué est ajouté, ce qui nuirait aux performances. En cas de dépassement, nous ré-allouerons cette zone par 10.
Comme nous avons réservé de la mémoire, il ne faut pas oublier de la libérer lors de la destruction de l’objet:
- ( void )dealloc
{
    free( delegates );
    [ hashs release ];
    [ super dealloc ];
}
Regardons maintenant la méthode utilisée pour ajouter un délégué:
- ( void )addDelegate: ( id )object
{
    NSString * hash;

    if( object == nil ) {
        return;
    }

    if( numberOfDelegates == sizeOfDelegatesArray ) {

        if( NULL == ( delegates = ( id * )realloc( delegates, ( sizeOfDelegatesArray + 10 ) * sizeof( id ) ) ) ) {

            // Error management...
        }

        sizeOfDelegatesArray += 10;
    }

    hash = [ [ NSNumber numberWithUnsignedInteger: ( NSUInteger )object ] stringValue ];

    if( [ hashs objectForKey: hash ] != nil ) {
        return;
    }

    delegates[ numberOfDelegates ] = object;

    [ hashs setObject: [ NSNumber numberWithUnsignedInteger: numberOfDelegates ] forKey: hash ];

    numberOfDelegates++;
}
Nous avons précédemment alloué un emplacement pour 10 délégués. A supposer qu’il y en ait 10, et que nous ajoutons un onzième, nous augmentons la zone mémoire de 10, grâce à la fonction ‘realloc’.
Nous prenons ensuite l’adresse mémoire de l’objet, sous forme de chaîne de charactère, et nous contrôlons que l’objet n’a pas déjà ajouté comme délégué. De cette façon, il est impossible d’ajouter plusieurs fois le même objet comme délégué.
Il ne nous reste plus qu’à stocker le pointeur vers notre objet, son adresse mémoire avec sa position dans le tableau de pointeur, et augmenter de 1 la variable contenant le nombre de délégués.
Sur le même principe, voici la fonction permettant d’enlever un délégué.
- ( void )removeDelegate: ( id )object
{
    NSString * hash;
    NSUInteger index;
    NSUInteger i;

    if( object == nil || numberOfDelegates == 0 ) {
        return;
    }

    hash = [ [ NSNumber numberWithUnsignedInteger: ( NSUInteger )object ] stringValue ];

    if( [ hashs objectForKey: hash ] == nil ) {
        return;
    }

    index = [ [ hashs objectForKey: hash ] unsignedIntegerValue ];

    for( i = index; i < numberOfDelegates - 1; i++ ) {

        delegates[ i ] = delegates[ i + 1 ];
    }

    [ hash removeObjectForKey: hash ];

    numberOfDelegates--;
}
Le fonctionnement de cette méthode repose sur le même principe que la précédente, avec une petite subtilité.
A supposer que l’on aie 5 délégués, et que l’on enlève l’objet se trouvant en position 3 dans notre tableau de pointeur, nous aurons un trou. Pour éviter cela, nous décalons donc également tous les pointeurs se trouvant après celui que nous avons ôté.
Et en dernier lieu, la méthode permettant d’obtenir un tableau contenant tous les objets délégués.
- ( NSArray * )delegates
{
    NSUInteger i;
    NSMutableArray * delegatesArray;

    if( numberOfDelegates == 0 ) {

        return [ NSArray array ];
    }

    delegatesArray = [ NSMutableArray arrayWithCapacity: numberOfDelegates ];

    for( i = 0; i < numberOfDelegates; i++ ) {
        [ delegatesArray addObject: delegates[ i ] ];
    }

    return [ NSArray arrayWithArray: delegatesArray ];
}

Il s’agit d’une simple boucle sur notre tableau de pointeur, qui ajoute les objets pointés dans un objet de type ‘NSArray’.

7. Runtime et re-routage des méthodes

Nous avons vu plus haut que, pour déterminer si le délégué possède une certaine méthode, il fallait utiliser la méthode ‘respondsToSelector’ sur l’objet délégué.
if( [ delegate respondsToSelector: @selector( someMethod ) ] ) {}
Pour éviter de changer les habitudes de programmation des dévelopeurs Cocoa, nous allons rendre ceci possible sur notre objet ‘DelegateChain’.
Actuellement. le code ci-dessus n’a aucune chance de fonctionner, vu que notre objet ‘DelegateChain’, qui contient les délégués, ne possède bien sûr pas leurs méthodes à son étage.
Nous pouvons par contre parfaitement surcharger (re-déclarer) dans notre classe la méthode ‘respondToSelector’ (à l’origine, elle est déclarée dans la classe ‘NSObject’), pour lui affecter un comportement autre que celui par défaut.
- ( BOOL )respondsToSelector: ( SEL )selector
{
    NSUInteger i;

    for( i = 0; i < numberOfDelegates; i++ ) {
        if( [ delegates[ i ] respondsToSelector: selector ] == YES ) {
            return YES;
        }
    }

    return NO;
}
Nous effectuons donc une boucle sur notre tableau de pointeur, et contrôlons si l’un des délégué possède la méthode demandée. De cette manière, nous pouvons utiliser notre objet ‘DelegateChain’ comme s’il s’agissait d’un délégué normal et unique.
Pour que cela fonctionne, il nous faut également surcharger la méthode ‘methodSignatureForSelector’ (NSObject). Celle-ci permet à l’environnement runtime d’Objective-C d’obtenir les infos nécessaires sur une méthode particulière, comme son type de retour, ses arguments, etc.
- ( NSMethodSignature * )methodSignatureForSelector: ( SEL )selector
{
    NSUInteger i;

    for( i = 0; i < numberOfDelegates; i++ ) {
        if( [ delegates[ i ] respondsToSelector: selector ] == YES ) {
            return [ [ delegates[ i ] class ] instanceMethodSignatureForSelector: selector ];
        }
    }

    return nil;
}
Désormais, nous pouvons savoir si au moins un délégué possède une méthode spécifique. Mais comment appeler le ou les délégués contenant cette méthode?
Ici aussi, nous allons garder le même système d’appel que pour un délégué unique, à savoir que la méthode désirée sera appelée directement sur notre objet ‘DelegateChain’, qui devra se charger de re-router cet appel vers le ou les délégués concernés.
Pour ce faire, nous allons implémenter la méthode ‘forwardInvocation’ dans notre classe.
Cette dernière est automatiquement appelée par l’environnement runtime d’Objective-C lorsqu’une méthode est appelée sur un objet ne la possédant pas. Ceci afin de donner une dernière chance à l’objet en question de gérer l’erreur.
Le même genre de concept est utilisé dans de nombreux langages de programmation objet. On peut par exemple citer les méthodes virtuelles de C++ ou encore la méthode ‘__call’ en PHP5.
- ( void )forwardInvocation: ( NSInvocation * )invocation
{
    NSUInteger i;

    for( i = 0; i < numberOfDelegates; i++ ) {

        if( [ delegates[ i ] respondsToSelector: [ invocation selector ] ] == YES ) {
            [ invocation invokeWithTarget: delegates[ i ] ];
        }
    }
}
Le système de délégation multiple est ici complétement opérationnel. Pour en bénéficier dans une classe, il suffit désormais que cette dernière soit une sous-classe de ‘MultipleDelegateObject’. Aucune autre implémentation n’est nécessaire.

8. Conclusion

Un tel système permet de définir des classes ayant un nombre infini de délégués. Par contre, les objets intrinsèques du framework Cocoa, tels ‘NSWindow’, ne pourront pas bénéficier de ce système.
Il est par contre parfaitement envisageable d’implémenter ce système de délégation multiple sur des objets comme ‘NSWindow’.
Le language Objective-C permet en effet la définition de catégories, permettant d’ajouter des méthodes dans n’importe quel objet disponible, que celui-ci soit défini dans notre projet, ou qu’il fasse partie d’un framework du système. Par exemple:
@interface NSObject( MyCategory )

- ( void )sayHello;

@end

@implementation NSObject( MyCategory )

- ( void )sayHello
{
    NSLog( @"Hello world!" );
}

@end
Le code ci-dessus ajoute une méthode ‘sayHello’ dans la classe ‘NSObject’, faisant partie du framework Cocoa. Comme ‘NSObject’ est la classe de base pour toutes les classes Objective-C, toutes les classes disponibles possèdent maintenant une méthode ‘sayHello’.
Nous pourrions donc parfaitement ajouter les méthodes ‘addDelegate’, ‘removeDelegate’ et ‘delegates’ à l’objet ‘NSWindow’.
La seule limitation des catégories est qu’il est impossible d’ajouter des variables d’instance à une classe. Par contre, l’objet ‘NSWindow’ possède déjà une variable d’instance prévue pour le délégué. Il faudra simplement ne pas oublier de redéfinir la méthode ‘setDelegate’ de ‘NSWindow’ dans notre catégorie. L’utilisation d’une variable globale statique (donc dont l’accès est limité au fichier qui l’a déclarée) est également une possibilité envisageable pour stocker les chaînes de délégués.
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license can be found at: http://www.gnu.org/copyleft/fdl.html

You may also like

1 comment

Steve 11 avril 2011 at 19 h 59 min

Merci énormement pour cette article

Reply

Leave a Comment