c# - ¿Debo exponer IObservable<T> en mis interfaces?
.net system.reactive (4)
Mi colega y yo tenemos disputa. Estamos escribiendo una aplicación .NET que procesa grandes cantidades de datos. Recibe elementos de datos, los agrupa en grupos de acuerdo con algún criterio y procesa esos bloques.
Digamos que tenemos elementos de datos del tipo Foo
llegan a alguna fuente (desde la red, por ejemplo) uno por uno. Deseamos reunir subconjuntos de objetos relacionados de tipo Foo
, construir un objeto de tipo Bar
partir de cada subconjunto y procesar objetos de tipo Bar
.
Uno de nosotros sugirió el siguiente diseño. Su tema principal es exponer IObservable<T>
directamente desde las interfaces de nuestros componentes.
// ********* Interfaces **********
interface IFooSource
{
// this is the event-stream of objects of type Foo
IObservable<Foo> FooArrivals { get; }
}
interface IBarSource
{
// this is the event-stream of objects of type Bar
IObservable<Bar> BarArrivals { get; }
}
/ ********* Implementations *********
class FooSource : IFooSource
{
// Here we put logic that receives Foo objects from the network and publishes them to the FooArrivals event stream.
}
class FooSubsetsToBarConverter : IBarSource
{
IFooSource fooSource;
IObservable<Bar> BarArrivals
{
get
{
// Do some fancy Rx operators on fooSource.FooArrivals, like Buffer, Window, Join and others and return IObservable<Bar>
}
}
}
// this class will subscribe to the bar source and do processing
class BarsProcessor
{
BarsProcessor(IBarSource barSource);
void Subscribe();
}
// ******************* Main ************************
class Program
{
public static void Main(string[] args)
{
var fooSource = FooSourceFactory.Create();
var barsProcessor = BarsProcessorFactory.Create(fooSource) // this will create FooSubsetToBarConverter and BarsProcessor
barsProcessor.Subscribe();
fooSource.Run(); // this enters a loop of listening for Foo objects from the network and notifying about their arrival.
}
}
El otro sugirió otro diseño que su tema principal es usar nuestras propias interfaces de editor / suscriptor y usar Rx dentro de las implementaciones solo cuando sea necesario.
//********** interfaces *********
interface IPublisher<T>
{
void Subscribe(ISubscriber<T> subscriber);
}
interface ISubscriber<T>
{
Action<T> Callback { get; }
}
//********** implementations *********
class FooSource : IPublisher<Foo>
{
public void Subscribe(ISubscriber<Foo> subscriber) { /* ... */ }
// here we put logic that receives Foo objects from some source (the network?) publishes them to the registered subscribers
}
class FooSubsetsToBarConverter : ISubscriber<Foo>, IPublisher<Bar>
{
void Callback(Foo foo)
{
// here we put logic that aggregates Foo objects and publishes Bars when we have received a subset of Foos that match our criteria
// maybe we use Rx here internally.
}
public void Subscribe(ISubscriber<Bar> subscriber) { /* ... */ }
}
class BarsProcessor : ISubscriber<Bar>
{
void Callback(Bar bar)
{
// here we put code that processes Bar objects
}
}
//********** program *********
class Program
{
public static void Main(string[] args)
{
var fooSource = fooSourceFactory.Create();
var barsProcessor = barsProcessorFactory.Create(fooSource) // this will create BarsProcessor and perform all the necessary subscriptions
fooSource.Run(); // this enters a loop of listening for Foo objects from the network and notifying about their arrival.
}
}
¿Cuál crees que es mejor? ¿Exponer IObservable<T>
y hacer que nuestros componentes creen nuevas secuencias de eventos de los operadores Rx, o definir nuestras propias interfaces de editor / suscriptor y usar Rx internamente si es necesario?
Aquí hay algunas cosas a considerar sobre los diseños:
En el primer diseño, el consumidor de nuestras interfaces tiene todo el poder de Rx a su alcance y puede realizar cualquier operador de Rx. Uno de nosotros afirma que esto es una ventaja y el otro afirma que esto es un inconveniente.
El segundo diseño nos permite utilizar cualquier arquitectura de publicador / suscriptor bajo el capó. El primer diseño nos une a Rx.
Si deseamos utilizar el poder de Rx, se requiere más trabajo en el segundo diseño porque necesitamos traducir la implementación personalizada del editor / suscriptor a Rx y viceversa. Requiere escribir código de pegamento para cada clase que desee realizar algún procesamiento de eventos.
En el primer diseño, el consumidor de nuestras interfaces tiene todo el poder de Rx a su alcance y puede realizar cualquier operador de Rx. Uno de nosotros afirma que esto es una ventaja y el otro afirma que esto es un inconveniente.
Estoy de acuerdo con la disponibilidad de Rx como una ventaja. Enumerar algunas razones por las que es un inconveniente podría ayudar a determinar cómo abordarlas. Algunas de las ventajas que veo son:
- Mientras Yam y Christoph se rozaron, IObservable / IObserver está en mscorlib a partir de .NET 4.0, por lo que (con suerte) se convertirá en un concepto estándar que todos entenderán de inmediato, como eventos o IEnumerable.
- Los operadores de Rx. Una vez que necesite componer, filtrar o manipular de otro modo posibles flujos múltiples, estos se vuelven muy útiles. Probablemente se encontrará haciendo este trabajo de alguna forma con sus propias interfaces.
- El contrato de Rx. La biblioteca Rx hace cumplir un contrato bien definido y hace todo lo posible para hacer cumplir ese contrato. Incluso cuando necesite crear sus propios operadores,
Observable.Create
hará el trabajo para hacer cumplir el contrato (por lo que el equipo de Rx no recomienda implementarIObservable
directamente). - La biblioteca de Rx tiene buenas maneras de asegurar que termine en el hilo correcto cuando sea necesario.
He escrito mi parte de operadores donde la biblioteca no cubre mi caso.
El segundo diseño nos permite utilizar cualquier arquitectura de publicador / suscriptor bajo el capó. El primer diseño nos une a Rx.
No veo cómo la opción de exponer a Rx tiene mucha influencia, si es que la tiene, en la forma en que implementa la arquitectura bajo el capó más de lo que lo haría usar sus propias interfaces. Yo afirmaría que no debería estar inventando nuevas arquitecturas de pub / sub a menos que sea absolutamente necesario.
Además, la biblioteca Rx puede tener operadores que simplificarán las partes "debajo del capó".
Si deseamos utilizar el poder de Rx, se requiere más trabajo en el segundo diseño porque necesitamos traducir la implementación personalizada del editor / suscriptor a Rx y viceversa. Requiere escribir código de pegamento para cada clase que desee realizar algún procesamiento de eventos.
Si y no. Lo primero que pensaría si viera el segundo diseño es: "Eso es casi como IObservable; escribamos algunos métodos de extensión para convertir las interfaces". El código del pegamento se escribe una vez, usado en todas partes.
El código del pegamento es sencillo, pero si piensa que usará Rx, solo exponga IObservable y ahórrese la molestia.
Consideraciones adicionales
Básicamente, su diseño alternativo difiere en 3 formas clave de IObservable / IObserver.
- No hay manera de darse de baja. Esto puede ser simplemente un descuido al copiar a la pregunta. Si no es así, es algo que debes considerar agregar si vas por esa ruta.
- No hay una ruta definida para que los errores fluyan en sentido descendente (por ejemplo,
IObserver.OnError
). - No hay forma de indicar la finalización de un flujo (por ejemplo,
IObserver.OnCompleted
). Esto solo es relevante si sus datos subyacentes están destinados a tener un punto de terminación.
Su diseño alternativo también devuelve la devolución de llamada como una acción en lugar de tenerlo como un método en la interfaz, pero no creo que la distinción sea importante.
La biblioteca Rx fomenta un enfoque funcional. Su clase FooSubsetsToBarConverter
se FooSubsetsToBarConverter
mejor como método de extensión a IObservable<Foo>
que devuelve IObservable<Bar>
. Esto reduce el desorden ligeramente (por qué hacer una clase con una propiedad cuando una función funcionará bien) y se adapta mejor a la composición de estilo de cadena del resto de la biblioteca Rx. Podría aplicar el mismo enfoque a las interfaces alternativas, pero sin la ayuda de los operadores, puede ser más difícil.
En primer lugar, vale la pena señalar que IObservable<T>
es parte de mscorlib.dll
y el espacio de nombres del System
, y, por lo tanto, exponerlo sería de alguna manera equivalente a exponer IComparable<T>
o IDisposable
. Lo que equivale a elegir .NET como su plataforma, lo que parece que ya ha hecho.
Ahora, en lugar de sugerir una respuesta, quiero sugerir una pregunta diferente, y luego una mentalidad diferente, y espero (y confío) que pueda manejar desde allí.
Básicamente, usted está preguntando: ¿Queremos promover el uso disperso de los operadores de Rx en todo nuestro sistema? . Ahora, obviamente, eso no es muy atractivo, ya que probablemente trate a Rx como una biblioteca de terceros.
De cualquier manera, la respuesta no está en los diseños basales que propusieron, sino en los usuarios de esos diseños. Recomiendo dividir su diseño a niveles de abstracción y asegurarse de que el uso de los operadores Rx esté dentro de un solo nivel. Cuando hablo de niveles de abstracción, me refiero a algo similar al modelo OSI , solo en el mismo código de la aplicación.
Lo más importante, en mi libro, es no tomar el punto de vista de diseño de "Vamos a crear algo que se va a usar y dispersar en todo el sistema, por lo que debemos asegurarnos de hacerlo solo una vez y de la manera correcta, para Todos los años por venir " . Soy más bien un "Hagamos que esta capa de abstracción produzca la API mínima necesaria para que otras capas alcancen sus objetivos" .
Sobre la simplicidad de sus dos diseños, es realmente difícil de juzgar, ya que Foo
y Bar
no me dicen mucho sobre los casos de uso y, por lo tanto, los factores de legibilidad (que son, por cierto, diferentes de un caso de uso a otro).
Exponer IObservable<T>
no contamina el diseño con Rx de ninguna manera. De hecho, la decisión de diseño es exactamente la misma que la pendiente entre exponer un evento .NET de la vieja escuela o lanzar su propio mecanismo de publicación / sub. La única diferencia es que IObservable<T>
es el concepto más nuevo.
¿Necesitas una prueba? Mire F #, que también es un lenguaje .NET pero más joven que C #. En F # cada evento se deriva de IObservable<T>
. Honestamente, no veo ningún sentido abstraer un mecanismo / pub .NET perfectamente adecuado, es decir, IObservable<T>
, con su abstracción de pub / sub de cosecha propia. Solo expone IObservable<T>
.
Rodar tu propia abstracción de pub / sub es como aplicar patrones de Java a un código .NET para mí. La diferencia es que en .NET siempre ha habido un gran soporte de framework para el patrón Observer y simplemente no hay necesidad de rodar el suyo.
Otra alternativa podría ser:
interface IObservableFooSource : IFooSource
{
IObservable<Foo> FooArrivals
{
get;
}
}
class FooSource : IObservableFooSource
{
// Implement the interface explicitly
IObservable<Foo> IObservableFooSource.FooArrivals
{
get
{
}
}
}
De esta manera, solo los clientes que esperan un IObservableFooSource verán los métodos específicos de RX, los que esperan que un IFooSource o un FooSource no lo hagan.