views:

730

answers:

3

Hi,

I need to parse an XML record that represents a QuizQuestion. The "type" attribute tells the type of question. I then need to create an appropriate subclass of QuizQuestion based on the question type. The following code works ([auto]release statements omitted for clarity):

QuizQuestion *question = [[QuizQuestion alloc] initWithXMLString:xml];
if( [ [question type] isEqualToString:@"multipleChoiceQuestion"] ) {
    [myQuestions addObject:[[MultipleChoiceQuizQuestion alloc] initWithXMLString:xml];
}

//QuizQuestion.m
-(id)initWithXMLString:(NSString*)xml {
    self.type = ...// parse "type" attribute from xml
    // parse the rest of the xml
}

//MultipleChoiceQuizQuestion.m
-(id)initWithXMLString:(NSString*)xml {
    if( self= [super initWithXMLString:xml] ) {
        // multiple-choice stuff
    }
}

Of course, this means that the XML is parsed twice: once to find out the type of QuizQuestion, and once when the appropriate QuizQuestion is initialized.

To prevent parsing the XML twice, I tried the following approach:

// MultipleChoiceQuizQuestion.m
-(id)initWithQuizRecord:(QuizQuestion*)record {
    self=record; // record has already parsed the "type" and other parameters
    // multiple-choice stuff
}

However, this fails due to the "self=record" assignment; whenever the MultipleChoiceQuizQuestion tries to call an instance-method, it tries to call the method on the QuizQuestion class instead.

Can someone tell me the correct approach for parsing XML into the appropriate subclass when the parent class needs to be initialized to know which subclass is appropriate?

+1  A: 

I suppose you are using NSXMLParser to parse the XML string, and using some of the events it fires to parse the XML source. In MultipleChoiceQuizQuestion, you could overwrite the NSXMLParser's delegate methods, like:

- (void)parser:(NSXMLParser *)parser
    didStartElement:(NSString *)elementName
    namespaceURI:(NSString *)namespaceURI
    qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
{
    [super parser:parser
        didStartElement:elementName
        namespaceURI:namespaceURI
        qualifiedName:qName attributes:attributeDict]

    // Multiple-choice specific stuff goes here.
}

Then call self = [super initWithXMLString:xml] as suggested.

MrMage
A: 

If you want to return a different instance in your init, you need to do this:

-(id)init {

    if (iNeedToBeADifferentClass) {
         [self release];
         return [[MyOtherClass alloc] init];
    } else {
         if (self = [super init]) {
              // normal init stuff
         }
         return self;
    }
}

Using this, you could parse the XML in QuizQuestion, init the MultipleChoiceQuizQuestion with the parsed values and the extra chunk of XML that only MultipleChoiceQuizQuestion needs to parse.

Otherwise, you could use factory classes or some other pattern. It all depends on the structure of your XML, though — if your XML is structured with parsing in mind, it'll be a lot easier to do this in code.

iKenndac
This code looks hacky to me. [self release] is both dangerous and cruel, as you are forcing your object to commit suicide. It seems dangerous since you end up executing code within a class instance that's been dealocated. All joking aside, a better way to achieve similar functionality could be to use a factory function.
Tyler
There are (rare) instances where this methodology is valid and used (for instance NSManagedObjectContext returns a different class instance from initWithEntity:insertIntoManagedObjectContext:), but I agree that it's normally a bad idea. [self autorelease] would be safer, though.
iKenndac
A: 

Have you tried WonderXML: http://wonderxml.googlecode.com/

Luke Du