Réflexion : uniformiser le code pour différentes versions d'AppKit

ChachaChacha Membre
août 2008 modifié dans Objective-C, Swift, C, C++ #1
I)Contexte
II)Solutions peu pratiques
III)Une bonne solution
IV)Construction d'un framework
V)Les limites

Salut,

I)Contexte

Les mises à  jour d'AppKit (avec les versions majeures de MacOS) apportent régulièrement de nouvelles classes et méthodes. Parfois il s'agit de méthodes toute bêtes mais diablement pratiques, comme par exemple "boolValue" (MacOS 10.5) pour une chaà®ne de caractères, qui implémentait déjà  "intValue" ou "doubleValue". De telles méthodes sont très faciles à  implémenter soi-même, mais on regrette toujours de ne pas les avoir quand on développe avec rétro-compatibilité.

Ce qui serait pratique, c'est de pouvoir écrire un code qui utilise la méthode standard si elle est disponible, et une implémentation personnelle autrement.

Même si Cocoa nous offre la méthode "respondsToSelector:" pour tester si une méthode existe, cela peut vite devenir lourdingue.

II)Solutions peu pratiques

Plusieurs idées peuvent venir :
1)Utiliser une classe utilitaire


@implementation MyString
+(BOOL) boolValue:(NSString*)s
{
  return [s isEqualToString:@YES];//je simplifie pour l'exemple
}
@end

...

NSString* s = @YES;
BOOL b = [s respondsToSelector:@selector(boolValue)] ?
  [s boolValue] : [MyString boolValue:s]; //bof...


2)Utiliser une catégorie

@implementation NSString (MyExtensions)
-(BOOL) myBoolValue
{
  return [self respondsToSelector:@selector(boolValue)] ?
    [self boolValue] : [self isEqualToString:@YES];
}
@end

...

NSString* s = @YES;
BOOL b = [s myBoolValue];


les inconvénients sont multiples :
-test de respondsToSelector: à  chaque fois
-ajout d'une méthode non-standard pour encapsuler un appel standard (ce qui diminue la lisibilité du code, et en plus est inutile sous la version d'AppKit implémentant la fonction voulue...)

Non, ce que l'on voudrait pouvoir écrire, c'est

NSString* s = @YES;
BOOL b = [s boolValue]; //appelle une méthode à  nous sous 10.4

Et on veut que cela compile sous 10.4 et sous 10.5 !

III)Une bonne solution

-Objective-C permet d'ajouter des méthodes aux classes pendant l'exécution. On doit pouvoir s'en servir pour ajouter une méthode "boolValue" si elle fait défaut.
-Grâce aux catégories, on peut déclarer une extension de NSString contenant une méthode boolValue. Si la méthode existe déjà  (sous 10.5), il n'y a pas besoin de spécifier une implémentation.

Voyons déjà  comment ajouter une méthode à  une classe. Avec Objective-C 1.0, il fallait faire pas mal de travail avec class_addMethods. Objective-C 2.0 a apporté class_addMethod pour simplifier. Je veux que mon code compile avec OC1 (MacOS <= 10.4) et (OC2 MacOS >= 10.5), et que même compilé avec OC2, il puisse s'exécuter sous 10.4. J'ai mélangé ici directives de compilation et détection de fonction au run-time (voir message http://www.objective-cocoa.org/forum/index.php/topic,2815.msg27995.html#msg27995)


+(BOOL) addMethod:(SEL)aSelector toClass:(Class)targetClass
{
  //le sélecteur n'est ajouté que ***SI NECESSAIRE***
  BOOL result = NO;
  if (![targetClass instancesRespondToSelector:aSelector])
  {
    NSLog(@should add);
    Method method = class_getInstanceMethod(self, aSelector);
    if (method)
    {
      void* handle = dlopen([[[NSBundle mainBundle] executablePath] UTF8String], RTLD_LAZY | RTLD_NOLOAD);
      BOOL objc2Used = !handle ? NO : (dlsym(handle, "class_addMethod") != 0);
      #if OBJC_API_VERSION==2
      if (objc2Used)
      {
        NSLog(@class_addMethod);
        result = class_addMethod(targetClass,
          method_getName(method), method_getImplementation(method), method_getTypeEncoding(method));
        objc2Used = result;
      }
      #else
      NSLog(@class_addMethods);
      if (!objc2Used && method)
      {
        struct objc_method_list* methodList = calloc(1, sizeof(struct objc_method_list));
        if (methodList)
        {
          methodList->method_count = 1;
          methodList->method_list[0] = *method;
          class_addMethods(targetClass, methodList);
          result = YES;
        }
      }
      #endif
    }//end if (method)
  }//end if (![targetClass instancesRespondToSelector:aSelector])
  NSLog(@result = %d, result);
  return result;
}


Il me faut maintenant une classe pour implémenter une méthode "boolValue". Appelons-la "Bridge". Tant qu'à  faire, je crée directement une catégorie m'indiquant que je mets ici des métodes pour NSString. Appelons cette catégorie... NSString!

@interface Bridge : NSObject {}
+(BOOL) addMethod:(SEL)aSelector toClass:(Class)targetClass;
@end

@interface Bridge (NSString)
-(BOOL) boolValue;
@end

@implementation Bridge (NSString)
-(BOOL) boolValue
{
  NSString* selfAsString = (NSString*)self; //cette méthode sera attribuée à  la class NSString !
                                            //donc "self" sera bien une NSString.
  return [selfAsString isEqualToString:@YES];
}
@end

Il faudrait maintenant attribuer boolValue à  la classe NSString. Utilisons la méthode +(void)load de Bridge.

+(void) load
{
  [self addMethod:@selector(boolValue) toClass:[NSString class]];
}


Et bien, si on rassemble les morceaux... c'est fini !

IV)Construction d'un framework

Pour rendre l'ensemble encore plus pratique, modulaire et évolutif, un framework paraà®t bien adapté.
Je propose une classe principale déclinée en diverses catégories (une par classe Cocoa que l'on va étendre). Le +(void)load de Bridge va charger un +(void) loadDeMaCategorie de chaque catégorie, lequel se chargera des addMethod: toClass: spécifiques.

Je vous joint le framework.

V)Les limites

Discutons un peu des limites :
-j'ai l'impression d'avoir fait un truc un peu compliqué : y avait pas plus simple ? (par exemple avec du method swizzling, mais ça ne me paraà®t pas plus simple)
-je n'ai rien pu tester sous 10.4 (compilation et utilisation du framework). Quelqu'un peut confirmer que ça marche ?
-même si on pense pouvoir utiliser un tel système pour ajouter des classes au run-time, on aura un problème de headers/déclaration de ces classes pour les versions de AppKit les implémentant déjà . Ou alors il faut utiliser de la compilation conditionnelle, mais on commence à  s'éloigner de l'aspect "automagique".
-de même, on ne peut pas déclarer sous 10.4 des constantes existant sous 10.5, sinon bonjour les redéfinitions.

+
Chacha

Réponses

  • ChachaChacha Membre
    13:34 modifié #2
    ça intéresse pas grand monde on dirait  :)
    Plus sérieusement, quelqu'un a-t-il pu effectuer des tests sous 10.4 ?

    +
    Chacha
  • Philippe49Philippe49 Membre
    13:34 modifié #3
    dans 1219065906:

    quelqu'un a-t-il pu effectuer des tests sous 10.4 ?

    Pour ma part, je n'ai pas 10.4, désolé.
    Je vois que tu as utilisé la macro OBJC_API_VERSION ...
     
  • 13:34 modifié #4
    Non parce que plus personne n'utilise 10.4 :p

    Pourquoi toi qui justement fait du code pour 10.4 ne l'as tu pas testé ?
  • ChachaChacha Membre
    13:34 modifié #5
    dans 1219073665:

    Pourquoi toi qui justement fait du code pour 10.4 ne l'as tu pas testé ?

    Ben parce que je ne l'ai plus... j'ai revendu mon PB sous 10.4, quand j'ai acheté mon MBP sous 10.5
Connectez-vous ou Inscrivez-vous pour répondre.