views:

307

answers:

1

After reading the Key-Value Coding Programming Guide, the Key-Value Observing Programming Guide and the Model Object Implementation Guide, as well as reading many StackOverflow entries on the topic and experimenting with various modelling scenarios, I feel like I have a good grasp on how to model my data.

I end up using declared properties for all my attributes and to-one relationships, backed by private ivars. For read-only attributes which need to be privately writeable, I use the readonly attribute in the .h interface declaration, then re-declare the property with the readwrite attribute in a class extension declared in the .m file. Inside the class methods, I always use the property accessors with the dot syntax and never access the private ivars directly.

There is however one aspect which still leaves me puzzled: how to properly model to-many relationships, especially when the collection is to be publicly immutable, but privately mutable (i.e. consumers of the model object cannot add or remove objects to the collection, but the collection's content is managed privately by the class).

I do understand how to implement the KVC accessor methods for to-many relationships (countOf<Key>, objectsIn<Key>AtIndex, etc.) and this is the route I've been following so far.

However, I've seen some sample code that uses declared properties to expose the relationships, do not implement the KVC accessor methods, yet are still Key-Value observable. For example:

@interface MyModel : NSObject
{
  // Note that the ivar is a mutable array,
  // while the property is declared as an immutable array.
  @private NSMutableArray *transactions_;
}

@property (nonatomic, retain, readonly) NSArray transactions;

@end

--------------------

@implementation MyModel

@synthesize transactions = transactions_;

- (void)privateMethodThatManagesTransactions
{
  [[self mutableArrayValueForKey:@"transactions"] addObject:t];
}

@end

If a consumer object adds itself as an observer of a MyModel instance for the "transactions" key path, it will be notified whenever transactions are added or removed from the transactions collection (as long as the mutations are done via the mutableArrayValueForKey: method).

To me, this seems like the cleanest way to expose to-many relationships as I don't need to hand-code the collection KVC accessors and it keeps the code clean.

However, it doesn't seem to be the way that is promoted by the Apple documentation, and I can't help but wonder if the fact that it works is only an unreliable side-effect.

So before commiting to one technique or the other in my real-life model classes for a project I'm beginning to work on, I'd like to get the opinion and advice of experienced Cocoa developers.

So the question is: if I use properties to model to-many relationships, do I still need to implement the KVC accessor/mutator methods?

Update

Even when I declare a to-many property as readonly, like in the example above, external code can still call mutableArrayValueForKey:@"transactions" on the model object and mutate the collection. This seems to indicate that using declared properties for to-many relationships isn't the way to go, but I still feel like I don't quite get it...

+4  A: 

Yes.

There is however one aspect which still leaves me puzzled: how to properly model to-many relationships, especially when the collection is to be publicly immutable, but privately mutable ….

Easy: Declare the property as readonly in the header, then redeclare it as readwrite, copy in a class extension in the implementation file.

I do understand how to implement the KVC accessor methods for to-many relationships (countOf<Key>, objectsIn<Key>AtIndex, etc.) and this is the route I've been following so far.

There are mutative ones, too. With these, you don't need to use mutableArrayValueForKey:; instead, you can use the mutative accessors directly. You'll still get KVO notifications, because KVO wraps those methods the first time something adds itself as an observer for the property.

I have a list of the accessor selector formats, including the mutative accessors, on my blog.

Edit:

Even when I declare a to-many property as readonly, like in the example above, external code can still call mutableArrayValueForKey:@"transactions" on the model object and mutate the collection.

This is a good reason to make it a habit to use the mutative accessors and avoid mutableArrayValueForKey:. You won't send mutation messages from outside the class if you get a compiler warning (no such [public] method) any time you try it.

Despite the availability of mutableArrayValueForKey: and the risk that someone will use it, KVO-compliant properties are the way to go here.

Peter Hosey
Thanks for the quick answer. I have edited my question to make it more precise: if I use properties to model to-many relationships, do I still need to implement the KVC accessor/mutator methods?
Pascal Bourque
Yes. You can synthesize the getter, but the setter will be wrong if you want a mutable object in your ivar (@property(copy) gives you *immutable* copies), and @synthesize won't give you any of the precise accessors (like objectIn<Key>AtIndex:). So you still need to implement *at least* the setter yourself.
Peter Hosey
I'm still confused... If I need to implement the KVC methods (accessors and mutators), then what is the added value of declaring a property on top of them?
Pascal Bourque
1. A property declaration documents the public nature of the property (e.g., readonly) and that it is a KVC-compliant property. Documentation, especially free documentation, is a Good Thing. 2. It makes it easy to use property-access syntax (obj.foo), as the compiler will know that the foo property exists, so it won't print a warning.
Peter Hosey
Let me see if I got it right. To expose a to-many relationship where external code is not allowed to mutate the collection, I should:1. Declare a readonly property for the collection, of type NSArray (or NSSet if unordered).2. Privately implement the KVC mutative accessors (insert/remove)3. Refrain from using mutableArrayValueForKey: to prevent accidental mutation from outside of the model class. Always use the private mutative accessors.You also suggested implementing a property setter, but I suppose it is only required if I want to replace the collection instance by another one?
Pascal Bourque
KVC/KVO always requires a setter for a mutable property (and your property is mutable, just not declared so in the header). @synthesize will give you a setter, but that setter is wrong if you want the array in your ivar to be mutable, so it's better to implement your own than to have a broken one lying around waiting to cause breakage. Other than that, exactly right.
Peter Hosey
(That is, if you have a getter, you must also have a setter. You can have neither or both, but not one without the other.)
Peter Hosey
That's the cause of my confusion... Maybe it's my .Net background that affects the way I expect things to be, but I still don't get why a setter is required. The collection instance itself cannot be replaced by a different instance, i.e. I don't want to allow something like myObject.transactions = [[NSMutableArray alloc] init]. The array is to be instanciated once in the initializer. Only the *content* of the array can be modified. And in some cases, I want the content of the array to be modified only by the class who owns it. So I still don't get why a setter is required...
Pascal Bourque
“I still don't get why a setter is required.” It's required because KVC and KVO require it. You'll get an exception if you have only the getter or setter without the other.
Peter Hosey
“The collection instance itself cannot be replaced by a different instance …” It can and it will if you let the synthesized setter stand. Implement your own, and you can use setArray:/setSet: instead. (I do this.)
Peter Hosey
“And in some cases, I want the content of the array to be modified only by the class who owns it.” Only some? I cannot think of a good reason to do otherwise.
Peter Hosey