Friday, February 21, 2014

KVO Considered Harmful

Soroush Khanlou (via Marcel Weiher):

All of these nuances in the API cause KVO to embody what is known as a pit of failure rather than a pit of success. The pit of success is a concept that Jeff Atwood talks about. APIs should be designed so that they guide you into using them successfully. The should give you hints as to how to use them, even if they don’t explain why you should use them in that particular way.

KVO does none of those things. If you don’t understand the subtleties in the parameters, or if you forget any of the details in implementation (which I did, and only noticed because I went back to my code to reference it while writing this blogpost), you can cause horrible unintended behaviors, such as infinite loops, crashes, and ignored KVO notifications.

I wish Cocoa didn’t have APIs that require you to use KVO.

9 Comments

I love Cocoa, but it suffers from large swaths of functionality that works only if everyone implements everything perfectly and that is very prone to emergent failure when human fallibility compounds. KVO is like this. Core Data is like this. NSDocument loading and saving is a light version of this if you're aiming for perfect implementation of the full API, which you very well might not.

I don't mind hard things sometimes being hard to implement by necessity. I do mind bread-and-butter tasks being hard to implement correctly. Even people who have spent the past 5, 10 or even 15 years building Cocoa apps run into problems with these tasks continuously. Something in Apple's DNA makes providing a solution where the hardest formulation of the problem is tamed for maximum flexibility apparently very appealing.

@Jesper Yes, I thought Cocoa was supposed to make the bread-and-butter tasks easy. And take Core Data and NSDocument and try to combine them. There’s an out-of-the-box class that’s supposed to do this for you, but it doesn’t work with packages, which are almost mandatory these days. So maybe you implement your own NSDocument subclass for Core Data. If you haven’t done this before, and you’re just working from the docs, what are the odds that you can get this right within your first few tries, even if you’re a 10-year Cocoa veteran? Or, scratch that, say you’ve been shipping this subclass for a year. What is your confidence that it actually handles all the cases correctly?

There certainly are some issues with using KVO, but I feel most of them are taken care of when using Mike Ash's MAKVONotificationCenter. I wouldn't use KVO without it. Surely other patterns have their issues too. Especially the doing things correctly bit. (I've seen developer posting NSNotifications which they were supposed to observe only.) I'd say any pattern is worth considering for what your problem needs. KVO shouldn't be excluded by default.

Glad to hear others think like that. KVO and bindings were great in theory when added to the frameworks, but all the corner cases and undocumented behavior (and bugs) just made them a waste of time. After a couple of debugging sessions, I quickly went back to delegation, datasources, and notifications (which the author of that article seems to incorrectly think were later than KVO).

Unfortunately, Apple seems to have undone all the cleanup work they did on the frameworks in the 10.6 era, especially for NSDocument. It has a crapton of deprecated methods, and I defy anyone to explain exactly what code path gets used on a given platform (it depends on Info.plist, what methods are implemented, what SDK you compiled against, and the AppKit version at runtime). Adding their automatic restore + asynchronous load/save into that was just insane.

My pet peeve with Cocoa is delayed actions, e.g. performSelector:withObject:afterDelay: — AppKit seems to make frequent use of that to coalesce operations, presumably some are also caused by auto-released objects. This makes it hard to reason about the order in which things are happening, and except for CoreData and setNeedsDisplay:, I don’t think the documentation even mentions that stuff won’t happen immediately.

In general I think the documentation could do a lot to help clarify many of the issues I have with Cocoa, even the running time of the collection classes is undocumented…

@Allan: The running time of the collection classes is undocumented because they are class clusters. An NSDictionary with a shared key set has different characteristics from other NSDictionaries, and CFArray/NSArray switch implementations across different sizes. ( http://ridiculousfish.com/blog/posts/array.html )

Reasonable people can disagree on whether that's a good or bad idea, but with that sort of lack of visibility and control over the specific implementation, they can't document anything more the worst case, which might be wildly inappropriate for the actual performance. And if they do document it now, they don't have jurisdiction of other people's concrete subclasses.

At least KVO can be worked around by using a wrapper object (I agree with Johan, though I use my own wrapper, see https://github.com/cparnot/PARViewController/blob/master/PARObjectObserver.h).

But bindings, I really only use it for the most simplistic cases, e.g. preferences, or direct model-view connections. It's too easy to hit weird issues when goin beyond that, as bindings' calls are not really fired when you expect them.

@Jesper Documenting the worst-case is generally what is meant by “running time” (as in big O notation).

Documenting the running time needs to be done so that users can pick the proper container and access pattern, for example not repeatedly calling removeObject: on an ordered set, if that operation is expected to be linear.

I don’t think that NSArray’s optimizations (to beat worst-case) changes that.

@Allan The Core Foundation headers do document the big-O running times, at least for the primitive operations.

Stay up-to-date by subscribing to the Comments RSS Feed for this post.

Leave a Comment