2012-07-09 12 views
17

Mój kolega i ja mamy spór. Piszemy aplikację .NET, która przetwarza ogromne ilości danych. Odbiera elementy danych, grupuje je podzbiorami na bloki według pewnego kryterium i przetwarza te bloki.Czy powinienem wystawiać IObservable <T> na moich interfejsach?

Załóżmy, że elementy danych typu Foo docierają do niektórych źródeł (np. Z sieci) jeden po drugim. Chcemy zebrać podzbiory powiązanych obiektów typu Foo, skonstruować obiekt typu Bar z każdego takiego podzbioru i obiektów procesu typu Bar.

Jeden z nas zasugerował następujący projekt. Jego głównym tematem jest eksponowanie obiektów bezpośrednio z interfejsów naszych komponentów.

// ********* 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. 
    } 
} 

Druga sugeruje inną konstrukcję, że jego głównym tematem jest za pomocą naszych własnych interfejsów wydawca/subskrybent i korzystając Rx wewnątrz implementacji tylko w razie potrzeby.

//********** 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. 
    } 
} 

Który z nich jest lepszy? Wyświetlając IObservable<T> i tworząc nasze komponenty, tworzysz nowe strumienie zdarzeń od operatorów Rx, lub definiujesz własne interfejsy wydawcy/subskrybenta i wewnętrznie używasz Rx w razie potrzeby?

Oto kilka rzeczy do rozważenia o wzory:

  • W pierwszym projekcie konsument naszych interfejsów ma całą moc Rx na jego/jej palców i może wykonywać żadnych operatorów RX. Jeden z nas twierdzi, że jest to zaleta, a drugi twierdzi, że jest to wadą.

  • Drugi projekt pozwala nam używać dowolnej architektury wydawcy/subskrybenta pod maską. Pierwszy projekt wiąże nas z Rx.

  • Jeśli chcemy korzystać z mocy Rx, wymaga to więcej pracy w drugim projekcie, ponieważ musimy przetłumaczyć niestandardową implementację wydawcy/subskrybenta na Rx iz powrotem. Wymaga napisania kodu kleju dla każdej klasy, która chce wykonać pewne przetwarzanie zdarzeń.

+3

Podoba mi się sposób, w jaki rozpoczynasz to pytanie. "Ja i mój kolega mamy spór.". +1. –

+3

Dlaczego nie narażać wszystkich rzeczy IObservable jako * metod rozszerzenia *, które dbają o cały ten "kod kleju".Zachowuje wszystkie elementy IObs oddzielnie od modeli obiektów, jednocześnie oferując opcję. 'public IObservable AsObservable (to IPublisher wydawca)' lub coś podobnego – Will

+4

Zrobiłeś dobrą robotę zadając pytanie w sposób zrównoważony. –

Odpowiedz

14

Wystawianie IObservable<T> nie zanieczyszcza zanieczyszcza projekt z Rx w jakikolwiek sposób. W rzeczywistości decyzja projektowa jest dokładnie taka sama, jak oczekiwana między wystawieniem starego szkolnego wydarzenia .NET lub przeniesieniem własnego mechanizmu pub/sub. Jedyna różnica polega na tym, że IObservable<T> jest nowszą koncepcją.

Potrzebujesz dowodu? Spójrz na F #, który jest również językiem .NET, ale młodszym od C#. W języku F # każde zdarzenie pochodzi od IObservable<T>. Szczerze mówiąc, nie widzę sensu w abstrakcjonowaniu idealnie pasującego mechanizmu/pubu .NET - czyli IObservable<T> - z dala od twojej domowej wersji abstrakcji pub/sub. Po prostu wyeksponuj IObservable<T>.

Przetwarzanie własnego streszczenia pub/sub jest jak stosowanie wzorów Java do kodu .NET do mnie. Różnica polega na tym, że w .NET zawsze istniała wspaniała obsługa ram dla wzorca Observer i po prostu nie ma potrzeby stosowania własnych.

0

Inną alternatywą może być:

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

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

ten sposób tylko klienci, oczekującym IObservableFooSource zobaczą RX-specyficzne metody te, które oczekują IFooSource lub FooSource nie będzie.

8

Przede wszystkim warto zauważyć, że IObservable<T> jest częścią mscorlib.dll i nazw System, a tym samym naraża byłoby nieco równoważne narażając IComparable<T> lub IDisposable. Co jest równoznaczne z wybieraniem platformy .NET, którą już wykonałeś.

Zamiast sugerować odpowiedź, chcę zasugerować inne pytanie, a następnie inny sposób myślenia i mam nadzieję (i zaufanie), że uda Ci się nim zarządzać.

Zasadniczo pytasz: Czy chcemy promować rozproszone używanie operatorów Rx w całym naszym systemie?.Teraz oczywiście nie jest to zbyt zachęcające, ponieważ prawdopodobnie traktujesz Rx jako bibliotekę innej firmy.

Tak czy inaczej, odpowiedź nie leży w podstawowych projektach, które zaproponowaliście, ale w użytkownikach tych projektów. Zalecam rozbicie twojego projektu na poziomy abstrakcji i upewnienie się, że użycie operatorów Rx ma zasięg tylko na jednym poziomie. Kiedy mówię o poziomach abstrakcji, mam na myśli coś podobnego do OSI Model, tylko w tym samym kodzie aplikacji.

Najważniejszą rzeczą w mojej książce jest unikanie projektu z punktu widzenia "Stwórzmy coś, co będzie używane i rozproszone po całym systemie, więc musimy się upewnić, że zrobimy to tylko raz i dobrze, na wszystkie nadchodzące lata ". Jestem bardziej z "Niech ta warstwa abstrakcji wytworzy minimalne API niezbędne dla innych warstw do obecnie osiągnąć swoje cele".

O prostocie oba projekty, to rzeczywiście trudno ocenić, ponieważ Foo i Bar nie mów mi dużo o przypadków użycia, a więc czynniki odczytu (które, nawiasem mówiąc, różne od jednego użytku do innego).

2

W pierwszym projekcie konsument naszych interfejsów ma na wyciągnięcie ręki całą moc Rx i może wykonywać dowolne operatory Rx. Jeden z nas twierdzi, że jest to zaleta, a drugi twierdzi, że jest to wadą.

Zgadzam się z dostępnością Rx jako zaletą. Wymienienie kilku powodów, dla których jest to wada, może pomóc w ustaleniu sposobu rozwiązania problemu. Niektóre zalety widzę to:

  • Jak Yam i Christoph zarówno ocierały, IObservable/IObserver jest w mscorlib jak .NET 4.0, więc to będzie (miejmy nadzieję) stać się standardem pojęcie, że każdy od razu zrozumieć, jak wydarzenia lub IEnumerable.
  • Operatory Rx. Kiedy trzeba skomponować, przefiltrować lub w inny sposób manipulować potencjalnie wieloma strumieniami, stają się one bardzo pomocne. Prawdopodobnie przekonasz się tę pracę w jakiejś formie za pomocą własnych interfejsów.
  • Umowa z Rx. Biblioteka Rx egzekwuje dobrze zdefiniowaną umowę i wykonuje jak najwięcej egzekwowania tego kontraktu, jak tylko może. Nawet jeśli musisz utworzyć własnego operatora, Observable.Create wykona pracę w celu wyegzekwowania umowy (dlatego implementacja IObservable bezpośrednio nie jest zalecana przez zespół Rx).
  • Biblioteka Rx ma wiele sposobów, aby w razie potrzeby trafić na właściwy wątek.

Napisałem mój udział operatorów, których biblioteka nie obejmuje mojej sprawy.

Drugi projekt pozwala nam używać dowolnej architektury wydawcy/subskrybenta pod maską. Pierwszy projekt wiąże nas z Rx.

nie widzę, w jaki sposób wybór wystawiać Rx ma wiele, jeśli jakikolwiek, wpływ na sposób wdrożyć architekturę pod maską nic więcej niż przy użyciu własnych interfejsów będzie. Twierdzę, że nie powinieneś wymyślać nowych architektur pub/sub, chyba że jest to absolutnie konieczne.

Co więcej, biblioteka Rx może mieć operatorów, którzy upraszczają części "pod maską".

Jeśli chcemy wykorzystać moc Rx, wymaga więcej pracy w drugim projekcie, ponieważ musimy przełożyć realizację niestandardowych wydawca/abonenta Rx iz powrotem. Wymaga napisania kodu kleju dla każdej klasy, która chce wykonać pewne przetwarzanie zdarzeń.

Tak i nie. Pierwszą rzeczą, którą mógłbym pomyśleć, gdybym zobaczył drugi projekt, to: "To prawie jak IObservable, napiszmy kilka metod rozszerzenia, aby przekonwertować interfejsy." Kod kleju jest zapisywany jeden raz, używany wszędzie.

Kod kleju jest prosty, ale jeśli myślisz, że będziesz używać Rx, po prostu ujawnij IObservable i zaoszczędź sobie kłopotów.

dalszych rozważań

Zasadniczo Twój alternatywny projekt różni się w 3 głównych sposobów z IObservable/IObserver.

  1. Nie można zrezygnować z subskrypcji. To może być tylko niedopatrzenie podczas kopiowania na pytanie. Jeśli nie, to jest coś, co należy wziąć pod uwagę dodając, jeśli jedziesz tą trasą.
  2. Nie ma zdefiniowanej ścieżki dla błędów w przepływie (np. IObserver.OnError).
  3. Nie można wskazać zakończenia strumienia (np. IObserver.OnCompleted). Jest to istotne tylko wtedy, gdy dane bazowe mają mieć punkt końcowy.

Twój alternatywny projekt zwraca również wywołanie zwrotne jako działanie, zamiast używać go jako metody interfejsu, ale nie sądzę, że rozróżnienie jest ważne.

Biblioteka Rx zachęca do podejścia funkcjonalnego. Twoja klasa FooSubsetsToBarConverter będzie lepiej pasować jako metoda rozszerzenia do IObservable<Foo>, która zwróci IObservable<Bar>. Zmniejsza to nieco bałaganu (dlaczego utworzyć klasę z jedną właściwością, gdy funkcja będzie działać dobrze) i lepiej pasuje do kompozycji stylu reszty biblioteki Rx. Można zastosować to samo podejście do alternatywnych interfejsów, ale bez pomocy operatora może być trudniejsze.

+1

+1 Dobra odpowiedź; umowa jest niezbędna. Są pewne gwarancje, które otrzymamy za darmo, jeśli pozostaniemy w Rx, w tym gwarancja kompozycji wolnej od skutków ubocznych z innymi monadami LINQ, takimi jak IEnumerable/IQueryable/IQbservable. Zdominowany pub/sub nie może mieć tych gwarancji w swoim projekcie, chyba że w zasadzie odtworzą LINQ/Rx (monady). –

Powiązane problemy