Wednesday, September 19, 2012 6:07 PM
I'd like to enumerate the factors to be considered when creating new Rx operators against Rx 2.0, to serve as a brief checklist. My intention is to gather feedback from the community and the Rx team on how you write robust Rx 2.0 operators.
- Know the existing Rx Design Guidelines. This discussion should be additive and/or corrective. References are included below.
- Know how to properly use existing Rx operators. This discussion is not about how to properly use existing Rx operators, it's about how to define new Rx operators.
For the purposes of this discussion, I'm defining "Rx operator" as either:
- Factory: A static method with zero or more IObservable<> parameters, optionally with additional parameters, that returns IObservable<>.
- Combinator: A static extension method with one or more IObservable<> parameters, optionally with additional parameters, that returns IObservable<>. (§6.3)
I'll start by creating a list of the factors that I think are important to consider when creating custom Rx operators, ordered in general from most important to least important.
This is not intended to be an exhaustive list, yet. I'll be happy to update it based on your feedback.
- Implement in terms of existing Rx operators and/or Observable.Create. Do not implement IObservable<>. (§§6.1, 6.2)
- Ensure the Rx grammar and assume that IObservable<> parameters are well-behaved. (§§4.1, 6.6)
- Ensure serialized notifications and assume that IObservable<> parameters are well-behaved. (§§6.7, 6.8, 4.2)
- IObservable<> models concurrency; therefore, assume that observable parameters execute concurrently when Subscribe is called. (§§4.4, 5.9)
- Assume that IObservable<> parameters are cold. (§5.10)
- Protect calls to user code. (§6.4)
- Do not catch exceptions thrown by observers; i.e., calls to OnNext, OnError and OnCompleted.
- Implement lazy (deferred) execution when generating a cold observable. Check arguments up front, but do not cause any side-effects until Subscribe is called. This includes scheduling work, iterating enumerable parameters, and mutating external state and parameters in general. (§6.16)
- Implement unsubscription, and do it properly. (§§6.17, 6.18)
- Parameterize scheduling when appropriate. (§§6.9, 6.10, 6.11, 5.4)
- Avoid deep call stacks caused by recursion. (§6.15)
- Watch for reentry when executing user code and assigning the result to a SerialDisposable. Use the double indirection pattern to avoid the effects of race conditions.
Performance and Memory
- Do not block. Execute asynchronously instead. (§6.14)
- Introduce concurrency only when necessary. (§§6.12)
- Choose a name that is semantically appropriate for the operator based on its business requirement and intended usage rather than behavioral details; e.g., TakeUntil is a better name than SecondStopsFirst. GetCustomerOrders is a better name than SendOrdersRequest or GetServerResponses.
- Do not include implementation details in names except to distinguish between otherwise ambiguous operators and parameters.
- Use pluralization to indicate that an observable may generate more than one notification; e.g., LoadImages.
- Consider naming an extension method that returns a hot observable as if it was a property; e.g., MouseMoves.
- Add an "Observable" suffix to distinguish an extension method from existing synchronous and asynchronous methods that have similar names; e.g., Stream.ReadObservable.
- Specify whether the generated observable is synchronous, asynchronous or concurrent when Subscribe is called.
- Specify whether the generated observable is hot or cold. Be specific about what, if any, side-effects occur when the operator is called and/or when Subscribe is called.
- Consider whether creating an async (C# 5; VB 11) method is a better fit. This may be true when the generated observable is a singleton (cardinality = 1) and callers aren't necessarily dependent on Rx. Any Task-returning operator is easily converted into an observable by callers via the ToObservable method. Note that when complex control flow is required or the operator depends on Task-returning methods; e.g., when await is useful, you don't necessarily have to define an async method. Instead, Rx 2.0 and Rx 1.1 Experimental define an overload of Observable.Create for defining async iterators with cardinality >= 1.
- Avoid using subjects explicitly, whenever possible. It's alright to use them implicitly if necessary; e.g., Publish.
- Avoid closing over local variables defined in the outer method body. Sometimes this pattern is useful, but often it's a mistake that causes an otherwise
cold observable to behave unpredictably because state is shared among multiple calls to