In the previous post, we looked at a nice Rx-implementation of an INPC subscriber by Richard Szalay, and added a couple more methods so that it could be used by a non-Rx-aware developer in a very simple manner.
In the comments, Richard pointed out that if you want to start to take advantage of other Rx facilities (like time-buffering), you would need to use the API in the way he originally intended (by getting the IObservable and working over that).
He's absolutely right, of course, and I think that this is a great example of a general "working smarter not harder" principle.
We should layer our APIs so that simple jobs we need to do a hundred times a day (subscribing to property notifications, in this case) have a very simple API, but with a "way in" to the more sophisticated behaviors. In general, how do we create a situation where incremental addition of behavior is (only) incrementally more complex?
That is such an important idea that I'm going to call it out in big letters in case there's someone driving by this post at 90mph.
Incrementally adding behavior should be (only) incrementally more complex for the developer using our APIs.
Also, I like the big quotation mark.
Now I've got that out of my system, let's look at the developer experience of this example in some more detail.
What we're trying to achieve is an easy step from the simple case:
viewModel.Subscribe(x => x.SomeProperty, SomePropertyChanged);
To the more complex case:
viewModel.GetPropertyChangeValues(x => x.TheProperty).Subscribe(SomePropertyChanged);
How similar are these bits of code, and can we see how and why we would transform from one to the other?
Well, first, they both use the viewModel as the subject. We've not moved from the subject being on the LHS of a method invocation, to being a parameter of a different method. This is a good thing. It means we can discover the other form just by typing "." and looking at the intellisense. I can feel another big quotation mark coming on.
How does your API appear in Intellisense? This is the number two method by which developers discover new APIs (after cutting and pasting from pre-existing code)
Second, we can recognize that we've just redistributed the parameters amongst the methods. We're clearly just breaking apart the bit where we specify which property we're interested in, from the bit where we subscribe to the change. Again, this is pretty discoverable. If we allow intellisense to autocomplete our GetPropertyChangeValues() method, we recognize the signature of the parameter it is asking for from our simple case. If we hit "." again, intellisense shows us the Subscribe() method; it is easy to see that we're adding an extra level of indirection in some way.
In comments, Richard suggested that it might be better to rename Subscribe() in our extension, but thinking about it some more, I am of the opinion that the symmetry in naming is essential for discoverability. Then I suggested that it might be better to break these subscription extensions out into another class, but now I disagree with myself (a frequent occurrence) – I think they need to be in the same place, again for discoverability.
The steeper part of the curve, for me at least, is GetPropertyChangeValues(). It doesn't tell us sufficiently clearly what it adds to the party. Specifically, there's no indication that this is the method that allows us to jump from the basic case over into the world of Rx. Intellisense does help us out (we can see all the other methods available when we hit "."), but we'd have to resort to the documentation to understand what has actually happened. That was fine when this was an Rx-only API – it is perfectly descriptive of the original intent, and you only had the one context; but it is now less helpful.
This is an important principle. If you add a new layer to your API, then the names in the original layer may no longer be fit-for-purpose. The corollary of this is that it is harder to retrofit a "simple" layer over an existing "complex" layer, without creating a steep learning curve, or adding an "intermediate" facade to smooth things out (which is problematic in itself, as it has a tendency to add more types and/or methods).
In this example, we're still in the API design stage, and free to make a breaking change, so I'm going to do exactly that. I want to propose a method name that is not just descriptive of the object you get back, but hints at the new context in which you can think about that object. I'm thinking of something like GetObservableValueFor().
viewModel.GetObservableValueFor(x => x.TheProperty).Subscribe(GetToWork);
This, I think, makes the transition to the world of Rx clearer (developers can see just by looking at the code we've written that this is the API to use to get an Observable thing), while still being descriptive of the purpose of the method.
API design is very difficult, particularly for "fundamentals" like pub/sub. Even a simple example like this throws open a huge number of possibilities, and arguments for and against various design and naming choices.
I think the best way to approach the problem is to go back to the developer use-cases, and look at the code you have to write as a client. Then, think about the transformations (mental and textual) you need to make to go from one related case to another. You can then debate which are "smoother" and more self-describing, not just in isolation, but in relation to one another – you are focused on the whole learning curve, not just the individual API.