Re: Redeclaring property types in mutable subclasses
Re: Redeclaring property types in mutable subclasses
- Subject: Re: Redeclaring property types in mutable subclasses
- From: Quincey Morris <email@hidden>
- Date: Tue, 29 Mar 2011 00:53:52 -0700
On Mar 28, 2011, at 22:54, Ken Thomases wrote:
> Do either of you really mean for that property to be read-write?
>
> I can see making a property read-write with an immutable type, and I can see (although I don't particularly like) making a read-only property return a mutable type. (Having properties of mutable type breaks encapsulation in a big way.) But it seems strange to me to make a read-write property of mutable type.
>
> Especially you, Quincey, when you've made a "sibling", mutable variant of the property. Is there really a setMutableGizmos: method? What does it mean?
Well, no, I didn't mean that, but ultimately it doesn't make a lot of difference. What I've actually done in the past is either:
>> @interface MutableWidget : Widget {}
>> @property (retain, readonly) NSMutableArray *mutableGizmos;
>> @end
>
or
>> @interface MutableWidget : Widget {}
>> @property (retain, readwrite) NSArray *gizmos; // redefined
>> @property (retain, readonly) NSMutableArray *mutableGizmos;
>> @end
>
In the first case, the caller can set the array's contents with a 'replace...' or a combination of 'remove...'/'insert...' methods. In the second case, the caller can also replace the array's contents with a simple assignment, which is a convenience and nothing more.
Either way, "mutableGizmos" is just a derived property of "gizmos". I don't think there's any particular dangers in making it readwrite, too or instead. There would of course need to be a custom setter for it, since there's no instance variable of its own to (accidentally) use @synthesize with, and that setter would simply update the "gizmos" property, ensuring KVO compliance.
In effect, the two properties are the same in the mutable subclass, except for return type. That's the whole point -- to get 2 different types for the same property. (If you choose to return the mutable proxy in the "mutableGizmo" case, though, they're not quite the same, but to me that's a plus.)
> Also, if you're using -mutableArrayValueForKey: without providing the mutation accessors, then that's probably asking for grief. Actually, you said something I didn't follow about a setter for the gizmos property, but that's read-only.
Sorry, it was a typo. I meant "getter".
> So, it's not clear to me how the proxy is supposed to mutate it (other than KVC's direct instance variable access, which I always disable). And without either a proper setter or the mutation accessors, you've violated encapsulation. Clients of your class can mutate your property without you being informed (unless you KVObserve your own property).
You may or may not need to know. If you don't, you haven't exactly violated encapsulation, since your accessors would just do the bidding of the caller anyway. :)
In my earlier response, I was forgetting that there doesn't seem to be a way of giving the mutable proxy access to your array without custom accessors, *except* via direct instance variable access.
However, I never intended to address whether or not there are custom accessors. It's a separate point so read on ...
> Here's what makes sense to me:
>
> @interface Widget : NSObject {}
> @property (retain, readonly) NSArray *gizmos;
>
> // optionally, any of:
> - (NSUInteger) countOfGizmos;
> - (id) objectInGizmosAtIndex:(NSUInteger)index;
> - (NSArray*) gizmosAtIndexes:(NSIndexSet*)indexes;
> - (void) getGizmos:(Gizmo**)buffer range:(NSRange)inRange;
>
> @end
>
>
> @interface MutableWidget : Widget {}
>
> // At least one of:
> - (void) insertObject:(Gizmo*)gizmo inGizmosAtIndex:(NSUInteger)index;
> - (void) insertGizmos:(NSArray *)gizmoArray atIndexes:(NSIndexSet *)indexes;
>
> // At least one of:
> - (void) removeObjectFromGizmosAtIndex:(NSUInteger)index;
> - (void) removeGizmosAtIndexes:(NSIndexSet *)indexes;
>
> // optionally, either of:
> - (void) replaceObjectInGizmosAtIndex:(NSUInteger)index withObject:(id)anObject;
> - (void) replaceGizmosAtIndexes:(NSIndexSet *)indexes withGizmos:(NSArray *)gizmoArray;
>
> @end
You don't *have* to make the accessors public API. When I first started doing this kind of thing a couple of years ago, I laboriously added the custom accessors (at least, the mutable ones) to the @interface, and I eventually took them all back out again:
-- It's a huge PITA to put these in the interface in the first place. Reams of boilerplate.
-- The custom accessors are significantly more keystrokes for callers to use than the generic NSArray/NSMutable array methods. I also found the correct order of the pieces to be really hard to remember.
-- Callers *still* can't use the full range of NSArray/NSMutableArray convenience methods (say, 'removeObjectsInRange:') without your providing a custom version *and* putting them in the interface *and* writing boilerplate code for them for *every* array property *and* updating them manually if you ever change a property name because Refactor certainly won't do it for you, *or* by providing NSArray/NSMutableArray proxies anyway (one of each). If the latter, it's easier and more consistent for callers just to use them all the time.
At least, that was my experience, and that was the end of making the custom accessors public (at least routinely).
> Then, clients of MutableWidget can either directly use those mutation accessors or they can invoke [aMutableWidget mutableArrayValueForKey:@"gizmos"] and mutate that. If you really want to wrap the latter in a convenience method, you can, but I don't think that constitutes a read-write property.
No, it was an oversight on my part (see above). It's better that "gizmos" should be readwrite, if you need that at all.
Personally, I prefer not to make it a caller responsibility to create mutable proxies. We have enough experience on this list with callers who've never heard of mutable proxies, or get into terrible KVO difficulties misusing them. A "mutableGizmos" property can simply solve that for you and your callers, forever. Compile-time safety prevents them from writing the wrong code.
> And I reiterate that one should almost always implement +(BOOL)accessInstanceVariablesDirectly { return NO; } on all one's classes.
+1
> If you're looking for guidance, I suggest contemplating what the framework classes do. Can you think of any mutable class which changes the type of one of the base class's properties to be mutable? Can you think of any which added a mutable variant of one of the base class's properties (as mutableGizmos is to gizmos)? Well, frankly, can you think of any framework class that has a property of mutable type?!?
I might be misunderstanding you, and I'm cheating a bit by picking a class method, but the obvious case is +[NSArray/NSMutableArray array] (etc). It simply punts on the issue by calling the result type 'id', but the caller is to assume that the returned object class *is* one or the other. The frameworks trades type safety for API brevity. The "mutableGizmos" approach adds a small amount of bulk, but gives you very good compile-time type safety.
> No. Mutable subclasses should provide setters or mutation accessors. That's the core of what they should add to the interface of the immutable super class.
Actually, I'd leave "setters or" out of this. Having a setter without mutation accessors is a potential performance problem. I'd say, "Mutable subclasses should provide direct ivar access or mutation accessors", and if your religion precludes the former, then just "Mutable subclasses should provide mutation accessors" period.
Sorry for the length of this reply. It's the penalty for writing too casual a response the first time.
_______________________________________________
Cocoa-dev mailing list (email@hidden)
Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com
Help/Unsubscribe/Update your Subscription:
This email sent to email@hidden