Il mio collega e io abbiamo una disputa. Stiamo scrivendo un'applicazione .NET che elabora enormi quantità di dati. Riceve elementi di dati, raggruppa sottoinsiemi di essi in blocchi secondo alcuni criteri ed elabora tali blocchi.
Diciamo che abbiamo elementi di dati di tipo Foo
che arrivano ad una fonte (dalla rete, per esempio) uno per uno. Desideriamo raccogliere sottoinsiemi di oggetti correlati di tipo Foo
, costruire un oggetto di tipo Bar
da ciascun sottoinsieme di questo tipo e oggetti di processo di tipo Bar
.
Uno di noi ha suggerito il seguente disegno. Il suo tema principale è l'esposizione di oggetti IObservable direttamente dalle interfacce dei nostri componenti.
// ********* 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.
}
}
L'altro ha suggerito un altro design il cui tema principale è l'utilizzo delle nostre interfacce editore / sottoscrittore e l'utilizzo di Rx all'interno delle implementazioni solo quando necessario.
//********** 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.
}
}
Quale pensi sia meglio? Esporre IObservable e rendere i nostri componenti creare nuovi flussi di eventi dagli operatori Rx, o definire le nostre interfacce editore / sottoscrittore e utilizzare Rx internamente, se necessario?
Ecco alcuni aspetti da considerare sui design:
-
Nel primo design il consumatore delle nostre interfacce ha tutta la potenza di Rx a portata di mano e può eseguire qualsiasi operatore Rx. Uno di noi afferma che questo è un vantaggio e l'altro afferma che questo è uno svantaggio.
-
Il secondo design ci consente di utilizzare qualsiasi architettura di editore / sottoscrittore sotto il cofano. Il primo design ci lega a Rx.
-
Se desideriamo utilizzare la potenza di Rx, è necessario più lavoro nel secondo progetto perché è necessario tradurre l'implementazione del publisher / sottoscrittore personalizzato in Rx e ritorno. Richiede la scrittura di codice colla per ogni classe che desidera eseguire l'elaborazione di un evento.