Подтвердить что ты не робот

Должен ли я открывать IObservable <T> на моих интерфейсах?

У моего коллеги и у меня есть спор. Мы пишем приложение .NET, которое обрабатывает огромные объемы данных. Он получает элементы данных, группирует их подмножества в блоки по определенному критерию и обрабатывает эти блоки.

Скажем, у нас есть элементы данных типа Foo, которые приходят с некоторым источником (из сети, например) один за другим. Мы хотим собрать подмножества связанных объектов типа Foo, построить объект типа Bar из каждого такого подмножества и объектов процесса типа Bar.

Один из нас предложил следующий дизайн. Его основная тема заключается в экспонировании объектов IObservable<T> непосредственно из интерфейсов наших компонентов.

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

Другой предложил другой дизайн, что его основная тема использует наши собственные интерфейсы издателя/подписчика и использует Rx внутри реализаций только тогда, когда это необходимо.

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

Какой, по вашему мнению, лучше? Экспозиция IObservable<T> и создание наших компонентов для создания новых потоков событий из Rx-операторов или определения наших собственных интерфейсов издателя/подписчика и при необходимости использования Rx внутри?

Вот некоторые вещи, которые следует учитывать в проектах:

  • В первом дизайне потребитель наших интерфейсов обладает всей мощью Rx на кончиках пальцев и может выполнять любые Rx-операторы. Один из нас утверждает, что это преимущество, а другое утверждает, что это недостаток.

  • Вторая конструкция позволяет нам использовать любую архитектуру издателя/подписчика под капотом. Первый проект связывает нас с Rx.

  • Если мы хотим использовать мощность Rx, это требует большей работы во втором проекте, потому что нам нужно перевести пользовательскую реализацию издателя/подписчика на Rx и обратно. Это требует написания кода клея для каждого класса, который хочет обработать некоторые события.

4b9b3361

Ответ 1

Экспозиция IObservable<T> никак не загрязняет дизайн с помощью Rx. Фактически, дизайнерское решение является тем же самым, что и в ожидании между экспонированием события старой школы .NET или переводом вашего собственного механизма pub/sub. Единственное отличие состоит в том, что IObservable<T> является более новой концепцией.

Нужно ли доказательство? Посмотрите на F #, который также является языком .NET, но моложе С#. В F # каждое событие происходит от IObservable<T>. Честно говоря, я не вижу смысла абстрагироваться от идеально подходящего механизма /pub/sub.NET - то есть IObservable<T> - прочь с вашей домашней паб-абстракцией. Просто выведите IObservable<T>.

Роллинг вашего собственного pub/sub абстракции похож на применение Java-шаблонов к .NET-коду для меня. Разница в том, что в .NET всегда была отличная поддержка фреймворков для шаблона Observer, и просто нет необходимости качать свои собственные.

Ответ 2

Прежде всего, стоит отметить, что IObservable<T> является частью пространства имен mscorlib.dll и System, поэтому экспозиция будет несколько эквивалентна экспонированию IComparable<T> или IDisposable. Это эквивалентно выбору .NET в качестве платформы, которую вы, похоже, уже сделали.

Теперь вместо того, чтобы предлагать ответ, я хочу предложить другой вопрос, а затем другое мышление, и я надеюсь (и надеюсь), что вы справитесь оттуда.

В основном вы спрашиваете: хотим ли мы продвигать разбросанное использование операторов Rx по всей нашей системе?. Теперь очевидно, что это не очень привлекательно, поскольку вы, вероятно, концептуально рассматриваете Rx как стороннюю библиотеку.

В любом случае ответ не лежит в базовых проектах, предложенных вами двумя, но в интересах тех проектов. Я рекомендую разбить свой проект на уровни абстракции и убедиться, что использование операторов Rx ограничено только одним уровнем. Когда я говорю об уровнях абстракции, я имею в виду нечто похожее на OSI Model, только в том же коде приложения.

Самое главное, что в моей книге - не учитывать дизайнерскую точку зрения "Дайте создать что-то, что будет использоваться и разбросано по всей системе, и поэтому нам нужно убедиться, что мы делаем это только один раз и просто право, на все последующие годы". Я больше "Пусть этот уровень абстракции создает минимальный API, необходимый для других слоев, чтобы в настоящее время достичь своих целей".

О простоте обоих ваших проектов, на самом деле трудно судить, поскольку Foo и Bar не говорят мне о случаях использования и, следовательно, коэффициенты читаемости (которые, кстати, отличается от одного варианта использования другим).

Ответ 3

В первом дизайне потребитель наших интерфейсов обладает всей мощью Rx на кончиках пальцев и может выполнять любые операторы Rx. Один из нас утверждает, что это преимущество, а другое утверждает, что это недостаток.

Я бы согласился с наличием Rx в качестве преимущества. Листинг некоторых причин, почему это недостаток, может помочь определить, как их решать. Некоторые преимущества, которые я вижу, следующие:

  • Как Ям, так и Кристоф оба приступили к работе, IObservable/IObserver находится в mscorlib по сравнению с .NET 4.0, поэтому он (надеюсь) станет стандартной концепцией, которую каждый сразу поймет, например, события или IEnumerable.
  • Операторы Rx. Как только вам нужно составить, фильтровать или иным образом манипулировать потенциально несколькими потоками, это становится очень полезным. Вероятно, вы, вероятно, переделаете эту работу в той или иной форме своими собственными интерфейсами.
  • Договор Rx. Библиотека Rx обеспечивает четко определенный контракт и делает как можно больше выполнения этого контракта. Даже когда вам нужно сделать своих собственных операторов, Observable.Create выполнит работу по обеспечению соблюдения контракта (поэтому внедрение IObservable напрямую не рекомендуется командой Rx).
  • Библиотека Rx имеет хорошие возможности для обеспечения того, чтобы вы попали в нужную нишу, когда это было необходимо.

Я написал свою долю операторов, где библиотека не распространяется на мое дело.

Вторая конструкция позволяет нам использовать любую архитектуру издателя/подписчика под капотом. Первый проект связывает нас с Rx.

Я не понимаю, как выбор Rx имеет много, если таковые имеются, влияние на то, как вы реализуете архитектуру под капотом, больше, чем использование ваших собственных интерфейсов. Я бы сказал, что вы не должны изобретать новые паб/sub-архитектуры, если это абсолютно необходимо.

Кроме того, библиотека Rx может иметь операторы, которые упростят части "под капотом".

Если мы хотим использовать мощность Rx, это требует большей работы во втором проекте, потому что нам нужно перевести пользовательскую реализацию издателя/подписчика на Rx и обратно. Это требует написания кода клея для каждого класса, который хочет обработать некоторые события.

Да и нет. Первое, что я подумал бы, если бы увидел второй дизайн: "Это почти как IObservable, давайте напишем некоторые методы расширения для преобразования интерфейсов". Код клея записывается один раз, используется везде.

Клей-код прост, но если вы считаете, что используете Rx, просто разоблачите IObservable и сберегите себя.

Дополнительные соображения

В принципе, ваш альтернативный дизайн отличается тремя ключевыми способами от IObservable/IObserver.

  • Отменить подписку невозможно. Это может быть просто недосмотр при копировании вопроса. Если нет, то что-то настоятельно рекомендуется добавить, если вы идете по этому маршруту.
  • Не существует определенного пути для ошибок для потока вниз по потоку (например, IObserver.OnError).
  • Невозможно указать завершение потока (например, IObserver.OnCompleted). Это актуально только в том случае, если для ваших базовых данных имеется точка завершения.

Ваш альтернативный дизайн также возвращает обратный вызов как действие, а не как его метод на интерфейсе, но я не думаю, что это различие важно.

Библиотека Rx поощряет функциональный подход. Ваш класс FooSubsetsToBarConverter лучше всего подходит как метод расширения для IObservable<Foo>, который возвращает IObservable<Bar>. Это немного уменьшает помехи (зачем создавать класс с одним свойством, когда функция будет делать все) и лучше подходит для композиций в стиле целых остальных библиотек Rx. Вы можете применить тот же подход к альтернативным интерфейсам, но без помощи операторов это может быть сложнее.

Ответ 4

Другой альтернативой может быть:

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

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

Таким образом, только клиенты, ожидающие IObservableFooSource, будут видеть специфичные для RX методы, ожидающие IFooSource или FooSource не будут.