Pregunta

Estoy haciendo algo de programación en Objective-C que implica analizar un NSXmlDocument y completar las propiedades de un objeto a partir del resultado.

La primera versión se veía así:

if([elementName compare:@"companyName"] == 0) 
  [character setCorporationName:currentElementText]; 
else if([elementName compare:@"corporationID"] == 0) 
  [character setCorporationID:currentElementText]; 
else if([elementName compare:@"name"] == 0) 
  ...

Pero no me gusta el if-else-if-else patrón que esto produce.Mirando a la switch declaración veo que sólo puedo manejar ints, chars etc y no objetos...Entonces, ¿hay algún patrón de implementación mejor que no conozco?

Por cierto, se me ocurrió una solución mejor para configurar las propiedades del objeto, pero quiero saber específicamente sobre if-else vs switch patrón en Objective-C

¿Fue útil?

Solución

Espero que todos me perdonen por arriesgarme aquí, pero me gustaría abordar la cuestión más general del análisis de documentos XML en Cocoa sin la necesidad de declaraciones if-else.La pregunta, como se indicó originalmente, asigna el texto del elemento actual a una variable de instancia del objeto de carácter.Como señaló jmah, esto se puede resolver mediante codificación clave-valor.Sin embargo, en un documento XML más complejo esto podría no ser posible.Consideremos, por ejemplo, lo siguiente.

<xmlroot>
    <corporationID>
        <stockSymbol>EXAM</stockSymbol>
        <uuid>31337</uuid>
    </corporationID>
    <companyName>Example Inc.</companyName>
</xmlroot>

Existen múltiples enfoques para abordar esto.Desde el principio, puedo pensar en dos que usan NSXMLDocument.El primero usa NSXMLElement.Es bastante sencillo y no implica en absoluto la cuestión de si-si no.Simplemente obtiene el elemento raíz y revisa sus elementos nombrados uno por uno.

NSXMLElement* root = [xmlDocument rootElement];

// Assuming that we only have one of each element.
[character setCorperationName:[[[root elementsForName:@"companyName"] objectAtIndex:0] stringValue]];

NSXMLElement* corperationId = [root elementsForName:@"corporationID"];
[character setCorperationStockSymbol:[[[corperationId elementsForName:@"stockSymbol"] objectAtIndex:0] stringValue]];
[character setCorperationUUID:[[[corperationId elementsForName:@"uuid"] objectAtIndex:0] stringValue]];

El siguiente usa el NSXMLNode más general, recorre el árbol y usa directamente la estructura if-else.

// The first line is the same as the last example, because NSXMLElement inherits from NSXMLNode
NSXMLNode* aNode = [xmlDocument rootElement];
while(aNode = [aNode nextNode]){
    if([[aNode name] isEqualToString:@"companyName"]){
        [character setCorperationName:[aNode stringValue]];
    }else if([[aNode name] isEqualToString:@"corporationID"]){
        NSXMLNode* correctParent = aNode;
        while((aNode = [aNode nextNode]) == nil && [aNode parent != correctParent){
            if([[aNode name] isEqualToString:@"stockSymbol"]){
                [character setCorperationStockSymbol:[aNode stringValue]];
            }else if([[aNode name] isEqualToString:@"uuid"]){
                [character setCorperationUUID:[aNode stringValue]];
            }
        }
    }
}

Este es un buen candidato para eliminar la estructura if-else, pero al igual que el problema original, aquí no podemos simplemente usar switch-case.Sin embargo, aún podemos eliminar if-else usando performSelector.El primer paso es definir un método para cada elemento.

- (NSNode*)parse_companyName:(NSNode*)aNode
{
    [character setCorperationName:[aNode stringValue]];
    return aNode;
}

- (NSNode*)parse_corporationID:(NSNode*)aNode
{
    NSXMLNode* correctParent = aNode;
    while((aNode = [aNode nextNode]) == nil && [aNode parent != correctParent){
        [self invokeMethodForNode:aNode prefix:@"parse_corporationID_"];
    }
    return [aNode previousNode];
}

- (NSNode*)parse_corporationID_stockSymbol:(NSNode*)aNode
{
    [character setCorperationStockSymbol:[aNode stringValue]];
    return aNode;
}

- (NSNode*)parse_corporationID_uuid:(NSNode*)aNode
{
    [character setCorperationUUID:[aNode stringValue]];
    return aNode;
}

La magia ocurre en el prefijo invokeMethodForNode:método.Generamos el selector en función del nombre del elemento y realizamos ese selector con un Nodo como único parámetro.Listo, hemos eliminado la necesidad de una declaración if-else.Aquí está el código para ese método.

- (NSNode*)invokeMethodForNode:(NSNode*)aNode prefix:(NSString*)aPrefix
{
    NSNode* ret = nil;
    NSString* methodName = [NSString stringWithFormat:@"%@%@:", prefix, [aNode name]];
    SEL selector = NSSelectorFromString(methodName);
    if([self respondsToSelector:selector])
        ret = [self performSelector:selector withObject:aNode];
    return ret;
}

Ahora, en lugar de nuestra declaración if-else más grande (la que diferenciaba entre nombre de empresa y ID de corporación), simplemente podemos escribir una línea de código.

NSXMLNode* aNode = [xmlDocument rootElement];
while(aNode = [aNode nextNode]){
    aNode = [self invokeMethodForNode:aNode prefix:@"parse_"];
}

Ahora me disculpo si me equivoqué en algo, ha pasado un tiempo desde que escribí algo con NSXMLDocument, es tarde en la noche y en realidad no probé este código.Entonces, si ve algo incorrecto, deje un comentario o edite esta respuesta.

Sin embargo, creo que acabo de mostrar cómo se pueden usar selectores con nombres adecuados en Cocoa para eliminar por completo las declaraciones if-else en casos como este.Hay algunas trampas y casos de esquina.El selector de ejecución:La familia de métodos solo toma métodos de 0, 1 o 2 argumentos cuyos argumentos y tipos de retorno son objetos, por lo que si los tipos de argumentos y el tipo de retorno no son objetos, o si hay más de dos argumentos, entonces tendría que usar un NSInvocation para invocarlo.Debe asegurarse de que los nombres de los métodos que genere no llamen a otros métodos, especialmente si el objetivo de la llamada es otro objeto, y este esquema de nombres de métodos en particular no funcionará en elementos con caracteres no alfanuméricos.Puede solucionar esto escapando de alguna manera los nombres de los elementos XML en los nombres de sus métodos, o creando un NSDictionary usando los nombres de los métodos como claves y los selectores como valores.Esto puede requerir bastante memoria y terminar llevando más tiempo.El envío de performSelector como lo describí es bastante rápido.Para declaraciones if-else muy grandes, este método puede incluso ser más rápido que una declaración if-else.

Otros consejos

Debería aprovechar la codificación de valores clave:

[character setValue:currentElementText forKey:elementName];

Si los datos no son de confianza, es posible que desees comprobar que la clave sea válida:

if (![validKeysCollection containsObject:elementName])
    // Exception or error

Si desea utilizar la menor cantidad de código posible y los nombres de los elementos y los configuradores tienen nombres de modo que si elementName es @"foo", entonces el setter es setFoo:, podría hacer algo como:

SEL selector = NSSelectorFromString([NSString stringWithFormat:@"set%@:", [elementName capitalizedString]]);

[character performSelector:selector withObject:currentElementText];

o posiblemente incluso:

[character setValue:currentElementText forKey:elementName]; // KVC-style

Aunque, por supuesto, serán un poco más lentos que usar un montón de declaraciones if.

[Editar:La segunda opción ya la mencionó alguien;¡ups!]

¿Me atrevo a sugerir el uso de una macro?

#define TEST( _name, _method ) \
  if ([elementName isEqualToString:@ _name] ) \
    [character _method:currentElementText]; else
#define ENDTEST { /* empty */ }

TEST( "companyName",      setCorporationName )
TEST( "setCorporationID", setCorporationID   )
TEST( "name",             setName            )
:
:
ENDTEST

Una forma de hacer esto con NSStrings es mediante el uso de NSDictionary y enumeraciones.Puede que no sea el más elegante, pero creo que hace que el código sea un poco más legible.El siguiente pseudocódigo está extraído de uno de mis proyectos:

typedef enum { UNKNOWNRESIDUE, DEOXYADENINE, DEOXYCYTOSINE, DEOXYGUANINE, DEOXYTHYMINE } SLSResidueType;

static NSDictionary *pdbResidueLookupTable;
...

if (pdbResidueLookupTable == nil)
{
    pdbResidueLookupTable = [[NSDictionary alloc] initWithObjectsAndKeys:
                          [NSNumber numberWithInteger:DEOXYADENINE], @"DA", 
                          [NSNumber numberWithInteger:DEOXYCYTOSINE], @"DC",
                          [NSNumber numberWithInteger:DEOXYGUANINE], @"DG",
                          [NSNumber numberWithInteger:DEOXYTHYMINE], @"DT",
                          nil]; 
}

SLSResidueType residueIdentifier = [[pdbResidueLookupTable objectForKey:residueType] intValue];
switch (residueIdentifier)
{
    case DEOXYADENINE: do something; break;
    case DEOXYCYTOSINE: do something; break;
    case DEOXYGUANINE: do something; break;
    case DEOXYTHYMINE: do something; break;
}

El if-else La implementación que tienes es la forma correcta de hacer esto, ya que switch no funcionará con objetos.Aparte de que tal vez sea un poco más difícil de leer (lo cual es subjetivo), no hay ninguna desventaja real en usar if-else declaraciones de esta manera.

Aunque no existe necesariamente una mejor manera de hacer algo así para un solo uso, ¿por qué usar "comparar" cuando puedes usar "isEqualToString"?Eso parecería ser más eficaz ya que la comparación se detendría en el primer carácter que no coincide, en lugar de pasar por todo el proceso para calcular un resultado de comparación válido (aunque pensándolo bien, la comparación podría ser clara en el mismo punto) - También aunque se vería un poco más limpio porque esa llamada devuelve un BOOL.

if([elementName isEqualToString:@"companyName"] ) 
  [character setCorporationName:currentElementText]; 
else if([elementName isEqualToString:@"corporationID"] ) 
  [character setCorporationID:currentElementText]; 
else if([elementName isEqualToString:@"name"] ) 

En realidad, existe una forma bastante sencilla de lidiar con declaraciones if-else en cascada en un lenguaje como Objective-C.Sí, puedes usar subclases y anulaciones, creando un grupo de subclases que implementen el mismo método de manera diferente, invocando la implementación correcta en tiempo de ejecución usando un mensaje común.Esto funciona bien si desea elegir una de varias implementaciones, pero puede resultar en una proliferación innecesaria de subclases si tiene muchas implementaciones pequeñas y ligeramente diferentes, como las que suele tener en sentencias if-else o switch largas.

En su lugar, factorice el cuerpo de cada cláusula if/else-if en su propio método, todos en la misma clase.Nombra los mensajes que los invocan de manera similar.Ahora cree un NSArray que contenga los selectores de esos mensajes (obtenidos usando @selector()).Convierta la cadena que estaba probando en los condicionales en un selector usando NSSelectorFromString() (es posible que primero deba concatenar palabras adicionales o dos puntos dependiendo de cómo nombró esos mensajes y si toman argumentos o no).Ahora realice usted mismo el selector usando performSelector:.

Este enfoque tiene la desventaja de que puede saturar la clase con muchos mensajes nuevos, pero probablemente sea mejor saturar una sola clase que toda la jerarquía de clases con nuevas subclases.

Publicar esto como respuesta a la respuesta anterior de Wevah: lo habría editado, pero todavía no tengo una reputación lo suficientemente alta:

desafortunadamente, el primer método falla en los campos que contienen más de una palabra, como xPosition.capitalizedString lo convertirá a Xposition, que cuando se combina con el formato le da setXposition:.Definitivamente no es lo que se quería aquí.Esto es lo que estoy usando en mi código:

NSString *capName = [elementName stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[[elementName substringToIndex:1] uppercaseString]];
SEL selector = NSSelectorFromString([NSString stringWithFormat:@"set%@:", capName]);

No es tan bonito como el primer método, pero funciona.

Se me ocurrió una solución que utiliza bloques para crear una estructura similar a un interruptor para los objetos.Allí va:

BOOL switch_object(id aObject, ...)
{
    va_list args;
    va_start(args, aObject);

    id value = nil;
    BOOL matchFound = NO;

    while ( (value = va_arg(args,id)) )
    {
        void (^block)(void) = va_arg(args,id);
        if ( [aObject isEqual:value] )
        {
            block();
            matchFound = YES;
            break;
        }
    }

    va_end(args);
    return matchFound;
}

Como puede ver, esta es una función C de la vieja escuela con una lista de argumentos variables.Paso el objeto a probar en el primer argumento, seguido de los pares case_value-case_block.(Recuerde que los bloques Objective-C son solo objetos). while El bucle sigue extrayendo estos pares hasta que el valor del objeto coincida o no queden casos (consulte las notas a continuación).

Uso:

NSString* str = @"stuff";
switch_object(str,
              @"blah", ^{
                  NSLog(@"blah");
              },
              @"foobar", ^{
                  NSLog(@"foobar");
              },
              @"stuff", ^{
                  NSLog(@"stuff");
              },
              @"poing", ^{
                  NSLog(@"poing");
              },
              nil);   // <-- sentinel

// will print "stuff"

Notas:

  • Esta es una primera aproximación sin ninguna comprobación de errores.
  • El hecho de que los manejadores de casos sean bloques requiere cuidado adicional en lo que respecta a la visibilidad, el alcance y la gestión de la memoria de las variables a las que se hace referencia desde dentro.
  • si olvidas al centinela, estás condenado :P
  • puede utilizar el valor de retorno booleano para activar un caso "predeterminado" cuando ninguno de los casos haya coincidido

La refactorización más común sugerida para eliminar declaraciones if-else o switch es introducir polimorfismo (ver http://www.refactoring.com/catalog/replaceConditionalWithPolymorphism.html).Eliminar dichos condicionales es más importante cuando están duplicados.En el caso del análisis XML como el de su ejemplo, básicamente está moviendo los datos a una estructura más natural para no tener que duplicar el condicional en otro lugar.En este caso, la declaración if-else o switch probablemente sea suficiente.

En este caso, no estoy seguro de poder refactorizar fácilmente la clase para introducir polimorfismo como sugiere Bradley, ya que es una clase nativa de Cocoa.En cambio, la forma de hacerlo en Objective-C es usar una categoría de clase para agregar un elementNameCode método para NSSting:

   typedef enum { 
       companyName = 0,
       companyID,  
       ...,
       Unknown
    } ElementCode;

    @interface NSString (ElementNameCodeAdditions)
    - (ElementCode)elementNameCode; 
    @end

    @implementation NSString (ElementNameCodeAdditions)
    - (ElementCode)elementNameCode {
        if([self compare:@"companyName"]==0) {
            return companyName;
        } else if([self compare:@"companyID"]==0) {
            return companyID;
        } ... {

        }

        return Unknown;
    }
    @end

En tu código, ahora puedes usar un interruptor en [elementName elementNameCode] (y obtenga las advertencias del compilador asociadas si olvida probar uno de los miembros de la enumeración, etc.).

Como señala Bradley, esto puede no valer la pena si la lógica se utiliza sólo en un lugar.

Lo que hemos hecho en nuestros proyectos donde necesitamos hacer este tipo de cosas una y otra vez es configurar un CFDictionary estático que asigna las cadenas/objetos para compararlos con un valor entero simple.Conduce a un código que se parece a este:

static CFDictionaryRef  map = NULL;
int count = 3;
const void *keys[count] = { @"key1", @"key2", @"key3" };
const void *values[count] = { (uintptr_t)1, (uintptr_t)2, (uintptr_t)3 };

if (map == NULL)
    map = CFDictionaryCreate(NULL,keys,values,count,&kCFTypeDictionaryKeyCallBacks,NULL);


switch((uintptr_t)CFDictionaryGetValue(map,[node name]))
{
    case 1:
        // do something
        break;
    case 2:
        // do something else
        break;
    case 3:
        // this other thing too
        break;
}

Si su objetivo es Leopard únicamente, puede usar NSMapTable en lugar de CFDictionary.

Al igual que Lvsti, estoy usando bloques para realizar un patrón de conmutación en objetos.

Escribí una cadena basada en bloques de filtro muy simple, que toma n bloques de filtro y realiza cada filtro en el objeto.
Cada filtro puede alterar el objeto, pero debe devolverlo.No importa qué.

NSObject+Funcional.h

#import <Foundation/Foundation.h>
typedef id(^FilterBlock)(id element, NSUInteger idx, BOOL *stop);

@interface NSObject (Functional)
-(id)processByPerformingFilterBlocks:(NSArray *)filterBlocks;
@end

NSObject+Funcional.m

@implementation NSObject (Functional)
-(id)processByPerformingFilterBlocks:(NSArray *)filterBlocks
{
    __block id blockSelf = self;
    [filterBlocks enumerateObjectsUsingBlock:^( id (^block)(id,NSUInteger idx, BOOL*) , NSUInteger idx, BOOL *stop) {
        blockSelf = block(blockSelf, idx, stop);
    }];

    return blockSelf;
}
@end

Ahora podemos configurar n FilterBlocks para probar los diferentes casos.

FilterBlock caseYES = ^id(id element, NSUInteger idx, BOOL *breakAfter){ 
    if ([element isEqualToString:@"YES"]) { 
        NSLog(@"You did it");  
        *breakAfter = YES;
    } 
    return element;
};

FilterBlock caseNO  = ^id(id element, NSUInteger idx, BOOL *breakAfter){ 
    if ([element isEqualToString:@"NO"] ) { 
        NSLog(@"Nope");
        *breakAfter = YES;
    }
    return element;
};

Ahora pegamos los bloques que queremos probar como cadena de filtros en una matriz:

NSArray *filters = @[caseYES, caseNO];

y puede realizarlo sobre un objeto

id obj1 = @"YES";
id obj2 = @"NO";
[obj1 processByPerformingFilterBlocks:filters];
[obj2 processByPerformingFilterBlocks:filters];

Este enfoque se puede utilizar para cambiar pero también para cualquier aplicación de cadena de filtros (condicional), ya que los bloques pueden editar el elemento y transmitirlo.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top