Objective-C : réifier les classes natives avec les Catégories

L'URL courte de cet article est : http://inagua.ch/C2128

Les Catégories en Objective-C : “Ça ressemble à de l’Objet, c’est encapsulé comme l’Objet… mais ce n’est pas de l’Objet”

Je suis en train de travailler sur une application iPad qui doit reprendre une partie du code source d’une précédente version de l’application.

Une des fonctionnalités que j’ai réutilisée est le blog. Il repose sur la digestion en lecture seule d’un flux JSON. Ce flux JSON, via l’API native iOS, retourne un NSDictionary (une hash map) par item, ou plus exactement un tableau (NSArray) d’items (NSDictionary).

Afin de limiter les modifications dans les sources que j’ai importées de la version antérieure, je me suis demandé si je ne pouvais pas utiliser directement les classes natives (NSArray et NSDictionary) au lieu de créer des classes dédiées (Blog et Post).
Et cela s’est finalement bien passé, et donne une solution plutôt séduisante grâce à un idiome du langage Objective-C : les Catégories.

Voir à la fin pour les sources du projet d’exemple.

En Ruby, on parle aussi de ré-ouverture de classe.

Etant donné que, contrairement au C++, Objective-C ne supporte pas l’héritage multiple, les catégories, avec les protocoles, sont un bon moyen d’étendre une classe.

Théorie

Pour ce qui est de l’aspect théorique des Catégories, je vous renvoie notamment au chapitre Catégories, extensions et sécurité (PDF) car il est en partie accessible sur le site de Pearson.

On peut notamment y trouver des arguments permettant de choisir entre héritage et catégorie :

  • La simplicité prévaut” : il est conseiller de minimiser la longueur de la chaine d’héritage
  • La création d’une sous-classe d’une classe implémentée sous forme d’un regroupement de classes, comme NSString, demande un travail important
  • Les catégories ont une visibilité plus grande” : pour le meilleur (on peut accéder aux méthodes définies sur n’importe quelle instance) et pour le pire (y compris celle dont on ne voudrait pas). Par exemple, une catégorie de NSString définissant une méthode retournant le domaine d’une adresse email peut être appelée sur n’importe quelle string, y compris des strings n’ayant rien à voir avec un email.
  • La redéfinition avec une catégorie présente des risques” : via une catégorie on peut redéfinir les méthodes de la classe étendue, ce qui est très puissant, et donc très risqué
  • L’ajout de variables d’instance exige une sous-classe” : mais on peut contourner cette contrainte avec des property (que l’on peut créer sur une catégorie).
  • Parfois, vous n’avez d’autre choix que de créer une sous-classe“… “Si vous devez étendre la même classe de différentes manières en différents endroits du programme
  • Certaines classes imposent la création de sous-classes“, notamment les classes abstraites qui ne peuvent pas être étendues via une catégorie.
  • Essayez la composition” : cela est vrai pour les langages orientés objets en général, de Privilégiez la composition à l’héritage.

Encore une fois, étant donné que, contrairement au C++, Objective-C ne supporte pas l’héritage multiple, les catégories, avec les protocoles, sont un bon moyen d’étendre une classe.

Pratique

Dans mon cas, un post a donc la structure JSON suivante :

{
 "date": "2013-04-04",
 "imageURL": "http://www.mydomain.com/upload/photo.jpg",
 "title": "Objective-C Category",
 "id": 6903,
 "author": "Jacques COUVREUR"
 }

J’ai donc créé une catégorie sur la classe NSDictionary qui permet de d’accéder aux valeurs sans avoir à spécifier la clef :

// NSDictionary+Post.m

@implementation NSDictionary (Post)

- (NSString*) postTitle { return [self objectForKey:KEY_TITLE]; }
- (NSString*) postAuthor { return [self objectForKey:KEY_AUTHOR]; }
- (NSString*) postDate { return [self objectForKey:KEY_DATE]; }
- (NSString*) postImageURL { return [self objectForKey:KEY_URL]; }
- (NSString*) postText { return [self objectForKey:KEY_TEXT]; }

Par expérience, j’ai pris l’habitude de préfixer le nom des messages (méthodes) de la catégorie par son nom, afin de les retrouver plus facilement par complétion et ainsi faciliter l’utilisation par la suite.
Cela permet aussi d’éviter les collisions avec des méthodes existantes sur la classe étendue.

J’ai également une méthode qui permet de prendre une version nouvellement téléchargée d’un post (un NSDictionary) et de la fusionner avec la version sauvegarder du même post (même ID) :

- (NSDictionary*) postUpdateWith:(NSDictionary*)downloaded {
  NSMutableDictionary* result = [NSMutableDictionary dictionaryWithDictionary:self];
  if (downloaded) {
    NSString* value = [downloaded postID];
    if (value) [result setValue:value forKey:KEY_POST_ID];
    [self updateFirst:result withSecond:downloaded forKey:KEY_TITLE];
    [self updateFirst:result withSecond:downloaded forKey:KEY_AUTHOR];
    [self updateFirst:result withSecond:downloaded forKey:KEY_DATE];
    [self updateFirst:result withSecond:downloaded forKey:KEY_IMGURL];
    [self updateFirst:result withSecond:downloaded forKey:KEY_TEXT];
  }
  return result;
}

J’ai ainsi fait émergé une entité de post, ses responsabilité, sans définir de classe mais en reposant sur une classe existante.
Et comme je le disais, ces posts sont recensés dans un tableau.

Pour être précis, le fonctionnement du blog est le suivant :

  1. A la consultation du blog, l’App affiche les posts sauvegardés
  2. L’App télécharge la liste de tous les posts en version allégée (plus de 400 posts, avec seulement le titre et l’ID)
  3. L’App affiche les 20 plus récents
  4. L’App télécharge le détail de ces 20 plus récents (texte et image)
  5. L’App sauvegarde ces 20 posts pour pouvoir fonctionner en mode offline.

En dehors du téléchargement, j’ai décidé de centraliser les fonctionnalités de gestion des posts dans une catégorie de NSArray. On dirait bien qu’une abstraction Blog vient d’émerger :

// NSMutableArray+Blog.m

@implementation NSMutableArray (Blog) 

// Find a post by its ID
- (NSDictionary*) blogPostWithID:(NSString*)wantedPostID {
  NSArray* posts = [NSArray arrayWithArray:self];
  for (NSDictionary* post in posts) {
    NSString* currentID = [post postID];
    if ([wantedPostID isEqualToString:currentID]) return post;
  }
  return nil;
}

// Does blog contains a post?
- (BOOL) blogContainsPost:(NSDictionary*)postInfo {
  return [self blogPostWithID:[postInfo postID]] != nil;
} 

// Return a new NSArray by merging self and the recentArray.
// Only the 'size' first elements of results are returned
- (NSMutableArray*) blogUpdateWith:(NSArray*)recentArray withFinalSize:(int)size {
  NSMutableArray* keep = [NSMutableArray arrayWithCapacity:size];
  int keepSize = 0;
  for (int i = 0; i < recentArray.count && keepSize < size; ++i) {
    NSDictionary* post = [recentArray objectAtIndex:i];
    if ([self blogContainsPost:post]) {
      NSDictionary* old = [self blogPostWithID:[post postID]];
      NSDictionary* updated = [old postUpdateWith:post];
      [self blogReplacePost:updated];
    } else {
      [keep addObject:post];
    }
    ++keepSize;
  }
  NSArray* result = [keep arrayByAddingObjectsFromArray:self];
  result = [result subarrayWithRange:NSMakeRange(0, MIN(result.count, size))];
  result = [result sortedArrayUsingComparator:^NSComparisonResult(NSDictionary* a, NSDictionary* b) {
      NSString* first = [a postDate];
      NSString* second = [b postDate];
      return [second compare:first];
    }];
  return [NSMutableArray arrayWithArray:result];
} 

// Replace existing post inside self with newPost(based on post ID)
- (void) blogReplacePost:(NSDictionary*)newPost {
  NSDictionary* old = [self blogPostWithID:[newPost postID]];
  int oldIndex = [self indexOfObject:old];
  [self replaceObjectAtIndex:oldIndex withObject:newPost];
}

J’ai décidé de créer et utiliser une méthode de remplacement plutôt que de mettre à jour directement le NSDictionary dans le NSArray, car l’API JSON retourne des NSDictionary imutables.

Dans ce cas précis, l’utilisation de catégories présente un autre intérêt.
Etant donné que les données sont peu volumineuses, en accès lecture seule et consultation “globale” (sans recherche), j’ai décidé de sauvegarder simplement les données dans les NSUserDefaults.
Or, l’enregistrement et le chargement de tableaux (NSArray) et hashmap (NSDictionary) est immédiate :

- (void) saveInUserDefaults {
  NSData* eventsData = [NSKeyedArchiver archivedDataWithRootObject:self.blogArray];
  NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]
  [defaults setObject:eventsData forKey:KEY_BLOG];
  [defaults synchronize];
}

- (void) loadFromUserDefaults {
  NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
  NSData* eventsData = [defaults objectForKey:KEY_BLOG];
  self.blogArray = [NSMutableArray arrayWithArray:(NSArray*)[NSKeyedUnarchiver unarchiveObjectWithData:eventsData]];
}

Sources

Vous pouvez télécharger le projet XCode d’exemple (avec les tests unitaires) : lire le README.txt pour les instructions.

Conclusion

Jusqu’à présent j’utilisais les catégories pour ajouter des méthodes helpers sur des classes natives, comme par exemple une méthode isBlank sur NSString.

Mais dans ce cas précis, les catégories constituent une alternative séduisante, et légitime (à mon sens) à de la Programmation Orientée Objet : elles procurent les avantages de cette dernière sans avoir à écrire la moindre nouvelle classe et donc utiliser directement les objets de l’API.

A moins que vous ayez un avis contraire ?…

L'URL courte de cet article est : http://inagua.ch/C2128

Une réflexion au sujet de « Objective-C : réifier les classes natives avec les Catégories »

  1. En effet, ça t’évite notamment toutes les méthodes de sérialization et déserialization vers les NSDict et NSArray.
    Par contre, ça peut poser des problèmes si tu as plusieurs “objets” représentés par des catégories. Tu n’as plus d’introspection, tous tes objets répondent aux mêmes méthodes. En effet ce n’est plus de l’objet!

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

*


*

Vous pouvez utiliser ces balises et attributs HTML : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>