views:

106

answers:

1

I'm parsing some input which produces a tree structure containing NSDictionary instances on the branches and NSString instance at the nodes.

After parsing, the whole structure should be immutable. I feel like I'm jumping through hoops to create the structure and then make sure it's immutable when it's returned from my method.

We can probably all relate to the input I'm parsing, since it's a query string from a URL. In a string like this:

a=foo&b=bar&a=zip

We expect a structure like this:

NSDictionary {
  "a" => NSDictionary {
    0 => "foo",
    1 => "zip"
  },
  "b" => "bar"
}

I'm keeping it just two-dimensional in this example for brevity, though in the real-world we sometimes see var[key1][key2]=value&var[key1][key3]=value2 type structures. The code hasn't evolved that far just yet.

Currently I do this:

- (NSDictionary *)parseQuery:(NSString *)queryString {
  NSMutableDictionary *params = [NSMutableDictionary dictionary];
  NSArray *pairs = [queryString componentsSeparatedByString:@"&"];

  for (NSString *pair in pairs) {
    NSRange eqRange = [pair rangeOfString:@"="];

    NSString *key;
    id value;

    // If the parameter is a key without a specified value
    if (eqRange.location == NSNotFound) {
      key = [pair stringByReplacingPercentEscapesUsingEncoding:NSASCIIStringEncoding];
      value = @"";
    } else {
      // Else determine both key and value
      key = [[pair substringToIndex:eqRange.location] stringByReplacingPercentEscapesUsingEncoding:NSASCIIStringEncoding];
      if ([pair length] > eqRange.location + 1) {
        value = [[pair substringFromIndex:eqRange.location + 1] stringByReplacingPercentEscapesUsingEncoding:NSASCIIStringEncoding];
      } else {
        value = @"";
      }
    }

    // Parameter already exists, it must be a dictionary
    if (nil != [params objectForKey:key]) {
      id existingValue = [params objectForKey:key];
      if (![existingValue isKindOfClass:[NSDictionary class]]) {
        value = [NSDictionary dictionaryWithObjectsAndKeys:existingValue, [NSNumber numberWithInt:0], value, [NSNumber numberWithInt:1], nil];
      } else {
        // FIXME: There must be a more elegant way to build a nested dictionary where the end result is immutable?
        NSMutableDictionary *newValue = [NSMutableDictionary dictionaryWithDictionary:existingValue];
        [newValue setObject:value forKey:[NSNumber numberWithInt:[newValue count]]];
        value = [NSDictionary dictionaryWithDictionary:newValue];
      }

    }

    [params setObject:value forKey:key];
  }

  return [NSDictionary dictionaryWithDictionary:params];
}

If you look at the bit where I've added FIXME it feels awfully clumsy, pulling out the existing dictionary, creating an immutable version of it, adding the new value, then creating an immutable dictionary from that to set back in place. Expensive and unnecessary?

I'm not sure if there are any Cocoa-specific design patterns I can follow here?

+1  A: 

Expensive and unnecessary?

Yes. Apple's Cocoa APIs regularly say they return an immutable object, but actually return a mutable subclass that's been cast to the immutable version. This is a standard operating procedure and an accepted Cocoa design principle. You just trust that your clients aren't going to cast it back to a mutable version and change things from underneath you.

From Cocoa Core Competencies: Object Mutability:

Receiving Mutable Objects

When you call a method and receive an object in return, the object could be mutable even if the method’s return type characterizes it as immutable. There is nothing to prevent a class from declaring a method to return an immutable object but returning a mutable object in its implementation. Although you could use introspection to determine whether a received object is actually mutable or immutable, you shouldn’t. Always use the return type of an object to judge its mutability.

See also: Cocoa Fundamentals Guide: Cocoa Objects.

Matt B.
Beautiful, thank you! I was considering doing this but thought it might have been seen the other way around – bad practice. Good to know.
d11wtq