April 30, 2014

Checklist for Creating Rx Operators

This blog post was originally posted here on the Rx forum back in 2012. I've noticed through reference tracking to my blog that it still gets some hits once in a while, so perhaps people are finding it to be useful. I figure it may get more visibility if I copy it to my blog. Note that I've made a few small elisions, corrections, clarifications and additions here, though I've left it essentially the same as the original post.

Prerequisites

  • Know the existing Rx Design Guidelines document.  You will find references to it throughout this checklist.
  • Know how to properly use existing Rx operators.  This checklist is not about how to properly use existing Rx operators, it's about how to define new operators for use in your business applications, or for inclusion into custom reactive frameworks that are based on the same design principles as Rx.

For the purposes of this checklist, an observable operator is any instance, static or extension method that returns IObservable<T>, including those that are typically considered to be static factory methods or combinators. (§6.3)

Also for the purposes of this checklist, the term observable refers to any local variable, parameter or member that is of the type IObservable<T> or a delegate to a function that returns an instance of IObservable<T>.

Checklist

Below is a categorized list of factors that I think are important to consider when creating new observable 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.

Behavior

  1. Implement your new operator by composing existing Rx operators together, whenever possible; otherwise, just use any overload of Observable.Create.  Do not implement IObservable<T> yourself.  (§§6.1, 6.2)
  2. Ensure that your operator satisfies the Rx grammar and assume that IObservable<T> parameters are well-behaved. (§§4.1, 6.6)
    OnNext* (OnCompleted|OnError)?
  3. Ensure that your operator serializes notifications and assume that IObservable<T> parameters are well-behaved. (§§6.7, 6.8, 4.2)
  4. IObservable<T> is a model for concurrency; therefore, assume that observable parameters execute concurrently when Subscribe is called. Ensure that your operator is thread-safe and, when merging or combining the notifications of multiple observable parameters, ensure that your output observable serializes notifications.  (§§4.4, 5.9)
  5. Assume that IObservable<T> parameters are cold. Convert their temperature before subscribing multiple times to avoid duplicating subscription side effects, if your operator does not have retry or repeat semantics.  (§5.10)
  6. Protect calls to user code.  (§6.4)
  7. Do not catch exceptions thrown by observers; i.e., calls to OnNext, OnError and OnCompleted.
  8. 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 or parameters.  (§6.16)
  9. Implement unsubscription, and do it properly.  (§§6.17, 6.18)
  10. Parameterize scheduling when appropriate.  (§§6.9, 6.10, 6.11, 5.4)
  11. Avoid deep call stacks caused by recursion.  (§6.15)
  12. 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

  1. Do not block.  Execute asynchronously instead.  (§6.14)
  2. Do not introduce extra asynchrony. Avoid the ObserveOn operator, whenever possible. Rely on your notification source, if any, to handle asynchrony itself; alternatively, consider offering parameterized scheduling, especially if you're generating your own notifications.  (§§5.5, 6.9, 6.12)

Semantics

  1. 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 SecondStopsFirstGetCustomerOrders is a better name than SendOrdersRequest or GetServerResponses.
  2. Do not include implementation details in names except to distinguish between otherwise ambiguous operators and parameters.
  3. Use pluralization to indicate that an observable may generate more than one notification; e.g., LoadImages.
  4. Consider naming an extension method returning a hot observable as if it's a property; e.g., MouseMoves.
  5. Add an "Observable" suffix to distinguish an operator from existing synchronous and asynchronous methods that have similar names; e.g., Stream.ReadObservable.

Documentation

  1. Specify whether the observable returned by your operator is synchronous, asynchronous or concurrent when Subscribe is called.
  2. Specify whether the observable returned by your operator is hot or cold.  Be specific about what, if any, side effects may occur when the operator is called and/or when Subscribe is called.

Style

  1. Consider whether creating an asynchronous method(C# 5; VB 11) 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 by callers into an observable 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 Release and Rx 1.1 Experimental define overloads of Observable.Create for defining async iterators.
  2. Avoid using subjects.  It's alright to use them implicitly if necessary; e.g., Publish.  (To Use Subject or Not To Use Subject)
  3. 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 Subscribe.

Tags:

Rx

Add comment