2012-07-09 11 views
17

Mi colega y yo tenemos una disputa. Estamos escribiendo una aplicación .NET que procesa grandes cantidades de datos. Recibe elementos de datos, los agrupa en subconjuntos en bloques de acuerdo con algún criterio y procesa esos bloques.¿Debo exponer IObservable <T> en mis interfaces?

Digamos que tenemos elementos de datos de tipo Foo llegando alguna fuente (de la red, por ejemplo) uno por uno. Deseamos juntar subconjuntos de objetos relacionados de tipo Foo, construir un objeto de tipo Bar desde cada subconjunto y procesar objetos del tipo Bar.

Uno de nosotros sugirió el siguiente diseño. Su tema principal es exponer objetos 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 está utilizando nuestras propias interfaces editor/suscriptor y el uso de Rx dentro de las implementaciones sólo 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? ¿Exponiendo IObservable<T> y haciendo que nuestros componentes creen nuevas secuencias de eventos de los operadores de Rx, o que defina nuestras propias interfaces de editor/suscriptor y que usen Rx internamente si es necesario?

Aquí están algunas cosas a considerar sobre los diseños:

  • En el primer diseño del consumidor de nuestras interfaces tiene todo el poder de Rx a su/sus dedos y puede realizar cualquier operador 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 editor/suscriptor bajo el capó. El primer diseño nos une a Rx.

  • Si deseamos usar la potencia de Rx, se requiere más trabajo en el segundo diseño porque necesitamos traducir la implementación personalizada de editor/suscriptor a Rx y viceversa. Requiere escribir código de pegamento para cada clase que desee hacer algún procesamiento de eventos.

+3

Me gusta la forma en que comienzas esta pregunta. "Mi colega y yo tenemos disputa". +1. –

+3

¿Por qué no exponer todas las cosas IObservable como * métodos de extensión * que se encargan de todo ese "código de pegamento".Mantiene todas las cosas IObs separadas de sus modelos de objetos, al tiempo que ofrece la opción. 'public IObservable AsObservable (este editor IPublisher )' o algo similar – Will

+4

Hiciste un buen trabajo al hacer la pregunta de forma equilibrada. –

Respuesta

14

Exponer IObservable<T> no contaminar el diseño con Rx de ninguna manera. De hecho, la decisión de diseño es exactamente la misma que está pendiente entre exponer un evento de .NET de la vieja escuela o implementar su propio mecanismo de publicación/publicación. La única diferencia es que IObservable<T> es el concepto más nuevo.

¿Necesita una prueba? Mire F # que también es un lenguaje .NET pero más joven que C#. En F #, cada evento deriva de IObservable<T>. Honestamente, no veo sentido en abstraer un mecanismo de pub/sub de .NET perfectamente adecuado - que es IObservable<T> - de distancia con la abstracción de pub/sub de cosecha propia. Solo exponer IObservable<T>.

Alinear tu propia abstracción de pub/sub se siente como aplicar patrones de Java al 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 hacer el suyo propio.

0

Otra alternativa podría ser:

interface IObservableFooSource : IFooSource 
{ 
    IObservable<Foo> FooArrivals 
    { 
     get; 
    } 
} 

class FooSource : IObservableFooSource 
{ 
    // Implement the interface explicitly 
    IObservable<Foo> IObservableFooSource.FooArrivals 
    { 
     get 
     { 
     } 
    } 
} 

esta manera sólo los clientes que esperan una IObservableFooSource verá los métodos RX-específicas, aquellas que esperar un IFooSource o una FooSource no lo hará.

8

En primer lugar, vale la pena señalar que IObservable<T> es parte de mscorlib.dll y la System espacio de nombres, y así exponerlo sería algo equivalente a exponer IComparable<T> o IDisposable. Lo cual es equivalente a elegir .NET como su plataforma, que parece que ya ha hecho.

Ahora, en lugar de sugerir una respuesta, quiero sugerir una pregunta diferente, y luego un modo de pensar diferente, y espero (y confío) que lo haga desde allí.

Básicamente está preguntando: ¿Deseamos promover el uso disperso de los operadores de Rx en todo nuestro sistema?.Ahora, obviamente, eso no es muy atractivo, ya que probablemente conceptualmente trates a Rx como una biblioteca de terceros.

De cualquier manera, la respuesta no está en los diseños basales que ustedes dos propusieron, sino en los usuarios de esos diseños. Recomiendo dividir tu diseño en niveles de abstracción y asegurarte de que el uso de los operadores Rx tiene un alcance de solo un nivel. Cuando hablo de niveles de abstracción, me refiero a algo similar al OSI Model, 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 va a ser utilizado y diseminado por todo el sistema, así que tenemos que asegurarnos de hacerlo solo una vez y justo, para todos los años ". Soy más un "Vamos a hacer que esta capa de abstracción produzca la API mínima necesaria para que otras capas actualmente logren sus objetivos".

Sobre la sencillez de ambos de sus diseños, en realidad es difícil de juzgar desde Foo y Bar no me dicen mucho acerca casos de uso, y por lo tanto los factores de legibilidad (que son, por cierto, diferentes de un uso caso a otro).

2

En el primer diseño, el consumidor de nuestras interfaces tiene toda la potencia de Rx al alcance de su mano y puede realizar cualquier operación 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 cuales es un inconveniente podría ayudar a determinar cómo abordarlas. Algunas de las ventajas que veo son:

  • Como Yam y Christoph tanto rozó, IObservable/IObserver está en mscorlib como de .NET 4.0, por lo que (esperemos) convertido en un concepto estándar que todo el mundo va a entender de inmediato, al igual que los eventos o IEnumerable.
  • Los operadores de Rx. Una vez que necesita componer, filtrar o manipular potencialmente múltiples flujos, estos se vuelven muy útiles. Probablemente te encuentres rehaciendo este trabajo de alguna forma con tus propias interfaces.
  • El contrato de Rx. La biblioteca Rx impone un contrato bien definido y hace todo lo posible por 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 Rx no recomienda la implementación directa de IObservable).
  • La biblioteca Rx tiene buenas maneras de asegurarse de que termine en el hilo correcto cuando sea necesario.

He escrito mi número de operadores donde la biblioteca no cubre mi caso.

El segundo diseño nos permite utilizar cualquier arquitectura de editor/suscriptor bajo el capó. El primer diseño nos une a Rx.

no veo cómo la elección de exponer Rx tiene poca o ninguna influencia sobre la forma de implementar la arquitectura bajo el capó no más que el uso de sus propias interfaces haría. Yo afirmaría que no debería inventar 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 la potencia de Rx, se requiere más trabajo en el segundo diseño porque tenemos que traducir la implementación personalizada de editor/suscriptor a Rx y viceversa. Requiere escribir código de pegamento para cada clase que desee hacer algún procesamiento de eventos.

Sí y no. Lo primero que pensaría si viera el segundo diseño es: "Eso es casi como IObservable, vamos a escribir algunos métodos de extensión para convertir las interfaces". El código de pegamento se escribe una vez, se usa en todas partes.

El código de pegamento es sencillo, pero si cree que usará Rx, solo exponga IObservable y ahórrese la molestia.

Otras consideraciones

Básicamente, su diseño alternativo difiere de 3 formas principales de IObservable/IObserver.

  1. No hay forma de darse de baja. Esto puede ser solo un descuido al copiar a la pregunta. De lo contrario, es algo a lo que se debe considerar si se sigue esa ruta.
  2. No hay una ruta definida para que los errores fluyan hacia abajo (por ejemplo, IObserver.OnError).
  3. No hay manera de indicar la finalización de una secuencia (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 método en la interfaz, pero no creo que la distinción sea importante.

La biblioteca Rx fomenta un enfoque funcional. Su clase FooSubsetsToBarConverter sería más adecuada como método de extensión a IObservable<Foo> que devuelve IObservable<Bar>. Esto reduce un poco el desorden (por qué crear una clase con una propiedad cuando una función funcionará bien) y se adapta mejor a la composición de estilo cadena del resto de la biblioteca Rx. Puede aplicar el mismo enfoque a las interfaces alternativas, pero sin los operadores para ayudar, puede ser más difícil.

+1

+1 Buena respuesta; el contrato es vital. Hay ciertas garantías que obtenemos de forma gratuita si permanecemos dentro de Rx, incluida la garantía de composición libre de efectos secundarios con otras mónadas LINQ como IEnumerable/IQueryable/IQbservable. Los pub/sub lanzados desde casa no pueden tener esas garantías en su diseño a menos que básicamente reinventen LINQ/Rx (mónadas). –

Cuestiones relacionadas