Чому суб'єкти не рекомендуються в .NET Реактивні розширення?


111

В даний час я переживаю рамки реактивних розширень для .NET, і я працюю через різні знайомі ресурси (головним чином http://www.introtorx.com )

У нашому додатку є ряд апаратних інтерфейсів, які виявляють мережеві кадри, це будуть мої IObservables, тоді у мене є різноманітні компоненти, які споживають ці кадри або виконують певну трансформацію даних і створюють новий тип кадру. Будуть також інші компоненти, яким потрібно відображати, наприклад, кожен n-й кадр. Я переконаний, що Rx стане корисним для нашого додатку, проте я бореться з деталями реалізації інтерфейсу IObserver.

Більшість ресурсів, які я читав (якщо не всі), сказали, що я не повинен сам реалізовувати інтерфейс IObservable, а використовувати одну із наданих функцій або класів. З моїх досліджень випливає, що створення Subject<IBaseFrame>файлу забезпечить мені те, що мені потрібно, я мав би свою єдину нитку, яка зчитує дані з апаратного інтерфейсу, а потім викликає функцію OnNext мого Subject<IBaseFrame>примірника. Потім різні компоненти IObserver отримуватимуть сповіщення від цього предмета.

Моя плутанина випливає з поради, яку наведено в додатку цього підручника, де написано:

Уникайте використання предметів. Rx є ефективно функціональною парадигмою програмування. Використання предметів означає, що ми зараз управляємо станом, який потенційно мутує. Зробити одночасно з мутуючим станом і асинхронним програмуванням дуже важко вийти. Крім того, багато операторів (методи розширення) були ретельно написані, щоб забезпечити правильне та послідовне життя підписок та послідовностей; коли ви вводите предмети, ви можете це порушити. У майбутніх випусках також може спостерігатися значне зниження продуктивності, якщо ви явно використовуєте теми.

Моя програма є досить важливою для продуктивності, я, очевидно, збираюся перевірити продуктивність використання шаблонів Rx, перш ніж перейти до виробничого коду; проте я переживаю, що роблю щось, що суперечить духу рамки Rx, використовуючи клас Subject і що майбутня версія фреймворку зашкодить продуктивності.

Чи є кращий спосіб робити те, що я хочу? Апаратний потік опитування буде працювати постійно, чи є спостерігачі, чи ні (буфер HW створить резервну копію в іншому випадку), тому це дуже гаряча послідовність. Потім мені потрібно передати отримані кадри декільком спостерігачам.

Будь-яка порада буде дуже вдячна.


1
Це дійсно допомогло моєму розумінню теми, я просто розбираю речі в голові про те, як їх використовувати у своїй програмі. Я знаю, що вони є правильним - у мене є конвеєр компонентів, дуже орієнтованих на натиск, і мені потрібно робити всі види фільтрування та виклику по потоку інтерфейсу для відображення в графічному інтерфейсі, а також буферизацію останнього отриманого кадру тощо і т. д. - Мені просто потрібно переконатися, що я це роблю правильно з першого разу!
Ентоні

Відповіді:


70

Гаразд, якщо ми ігноруємо мої догматичні способи і ігноруємо "предмети хороші / погані" всі разом. Давайте розглянемо проблемний простір.

Б'юсь об заклад, що у вас є 1 з 2 стилів системи, до яких потрібно вбудовуватися.

  1. Після надходження повідомлення система піднімає подію чи зворотній дзвінок
  2. Вам потрібно запитати систему, щоб побачити, чи є якесь повідомлення для обробки

Для варіанту 1 легко, ми просто обмотуємо його відповідним методом FromEvent, і ми закінчуємо. В паб!

Для варіанту 2 тепер нам потрібно розглянути, як ми опитуємо це, і як це зробити ефективно. Крім того, коли ми отримуємо значення, як ми його публікуємо?

Я б уявив, що ви хочете виділити тему для опитування. Ви не хочете, щоб якийсь інший кодер забивав ThreadPool / TaskPool і залишав вас у ситуації голодування ThreadPool. Крім того, ви не хочете клопоту переключення контексту (я думаю). Тож припустимо, що у нас є власна нитка, ми, мабуть, матимемо певний цикл "Хоча / Сон", у якому ми сидимо, щоб опитувати. Коли чек знаходить деякі повідомлення, ми публікуємо їх. Ну, все це ідеально підходить для Observable.Create. Зараз ми, мабуть, не можемо використовувати цикл "Хоча", як той звичай не дозволяє нам колись повертати одноразовий доступ, щоб дозволити скасування. На щастя, ви прочитали всю книгу, тому дотепні з рекурсивним плануванням!

Я думаю, щось подібне може спрацювати. # Не перевірено

public class MessageListener
{
    private readonly IObservable<IMessage> _messages;
    private readonly IScheduler _scheduler;

    public MessageListener()
    {
        _scheduler = new EventLoopScheduler();

        var messages = ListenToMessages()
                                    .SubscribeOn(_scheduler)
                                    .Publish();

        _messages = messages;
        messages.Connect();
    }

    public IObservable<IMessage> Messages
    {
        get {return _messages;}
    }

    private IObservable<IMessage> ListenToMessages()
    {
        return Observable.Create<IMessage>(o=>
        {
                return _scheduler.Schedule(recurse=>
                {
                    try
                    {           
                        var messages = GetMessages();
                        foreach (var msg in messages)
                        {
                            o.OnNext(msg);
                        }   
                        recurse();
                    }
                    catch (Exception ex)
                    {
                        o.OnError(ex);
                    }                   
                });
        });
    }

    private IEnumerable<IMessage> GetMessages()
    {
         //Do some work here that gets messages from a queue, 
         // file system, database or other system that cant push 
         // new data at us.
         // 
         //This may return an empty result when no new data is found.
    }
}

Причиною, що мені справді не подобається «Суб’єкти», є те, що, як правило, розробник не має чіткого дизайну проблеми. Зламати тему, ткнути її сюди і скрізь, а потім нехай здогад, який розгадує підтримку на WTF, продовжував. Під час використання методів Create / Generation тощо ви локалізуєте ефекти на послідовності. Ви можете бачити все це одним методом, і ви знаєте, що ніхто більше не викликає неприємний побічний ефект. Якщо я бачу предметні поля, тепер мені потрібно шукати всі місця в класі, яким він використовується. Якщо якийсь MFer виставляє одну публічно, то всі ставки знімаються, хто знає, як використовується ця послідовність! Async / Concurrency / Rx важко. Вам не потрібно робити це складніше, дозволяючи побічним ефектам і програмуванню причинності ще більше крутити голову.


10
Зараз я просто читаю цю відповідь, але відчув, що мені слід зазначити, що я ніколи не роздумую про відкриття інтерфейсу Subject! Я використовую його для забезпечення реалізації IObservable <> в межах закритого класу (який відкриває IObservable <>). Я точно можу зрозуміти, чому розкриття інтерфейсу Subject <> було б поганою річчю ™
Ентоні

Ей, вибачте, що є товстим, але я просто не дуже розумію ваш код. що виконують та повертають ListenToMessages () та GetMessages ()?
user10479

1
Для вашого особистого проекту @jeromerg це може бути добре. Однак на моєму досвіді розробники борються з WPF, MVVM, тестуванням дизайну графічного інтерфейсу, а потім введенням в Rx можуть ускладнити справи. Я спробував шаблон поведінки суб'єкта як власності. Однак я виявив, що це було набагато більш прийнятним для інших, якщо ми використовували стандартні властивості INPC, а потім використовували простий метод розширення, щоб перетворити це на IObservable. Крім того, вам знадобляться спеціальні прив’язки WPF для роботи зі своїми суб'єктами поведінки. Тепер ваша бідна команда також має вивчити WPF, MVVM, Rx та ваш новий фреймворк.
Лі Кемпбелл

2
@LeeCampbell, щоб сказати це як приклад вашого коду, нормальним способом було б те, що MessageListener будується системою (ви, мабуть, зареєструєте ім'я класу якось), і вам скажуть, що система потім викличе OnCreate () і OnGoodbye (), і буде викликати message1 (), message2 () та message3 (), коли повідомлення будуть генеровані. Схоже, що повідомленняX [123] викликає OnNext на тему, але чи є кращий спосіб?
Джеймс Мур

1
@JamesMoore, оскільки ці речі набагато простіше пояснити конкретними прикладами. Якщо ви знаєте про додаток з відкритим кодом для Android, який використовує Rx та Subject, тоді, можливо, я знайду час, щоб переконатися, чи зможу запропонувати кращий шлях. Я розумію, що не дуже корисно стояти на п'єдесталі і говорити, що Суб'єкти погані. Але я думаю, що такі речі, як IntroToRx, RxCookbook і ReactiveTrader, наводять різні рівні прикладу використання Rx.
Лі Кемпбелл

38

Як правило, вам слід уникати використання Subject, проте для того, що ви робите тут, я думаю, що вони працюють досить добре. Я задав подібне запитання, коли в навчальних посібниках Rx натрапив на повідомлення "уникати предметів".

Цитувати Дейва Секстона (від Rxx)

"Суб'єкти - це компоненти стану Rx. Вони корисні, коли вам потрібно створити подію, що можна спостерігати як поле або локальну змінну."

Я схильний використовувати їх як точку входу в Rx. Тож якщо у мене є якийсь код, який повинен сказати "щось трапилось" (як у вас), я б скористався Subjectфункцією " a" та зателефонував OnNext. Потім виставіть це IObservableна те, щоб підписатися на інших (ви можете використовувати AsObservable()свою тему, щоб переконатися, що ніхто не може подати тему і зіпсувати речі).

Ви також можете досягти цього за допомогою події .NET і використовувати FromEventPattern, але якщо я все IObservableодно збираюся перетворити подію , я не бачу вигоди від того, щоб мати подію замість Subject(що може означати, що я відсутній щось тут)

Однак те, що вам слід досить сильно уникати, - це передплата на " IObservablea" Subject, тобто не переходити Subjectна IObservable.Subscribeметод.


Навіщо взагалі потрібна держава? Як показано моєю відповіддю, якщо ви розділите проблему на окремі частини, вам не потрібно взагалі керувати станом. Тематика не повинна використовуватися в цьому випадку.
casperОдин

8
@casperOne Вам не потрібен стан поза Subject <T> або події (в обох є колекції речей, для виклику, спостерігачів або обробників подій). Я просто вважаю за краще використовувати Subject, якщо єдиною причиною додати подію є обведення її з FromEventPattern. Окрім зміни схем винятків, яка може бути важливою для вас, я не бачу користі уникнути таким чином тему. Знову ж таки, я, можливо, пропущу щось інше, що подія краща перед Темою. Згадка про державу була лише частиною цитати, і здавалося, що краще залишити її. Можливо, ясніше без цієї частини?
Вілька

@casperOne - але ви також не повинні створювати подію лише для того, щоб завершити її з FromEventPattern. Це очевидно жахлива ідея.
Джеймс Мур

3
Я детальніше пояснив свою цитату в цій публікації в блозі .
Дейв Секстон

Я схильний використовувати їх як точку входу в Rx. Це мене вдарило цвяхом по голові. У мене є ситуація, коли існує API, який при виклику генерує події, які я хотів би пройти по трубопроводу для реактивної обробки. Тема стала для мене відповіддю, оскільки FromEventPattern, здається, не існує в AFXIC RxJava.
скорпіодавг

31

Часто керуючи Subject, ви насправді просто повторюєте функції, які вже є в Rx, і, ймовірно, не настільки надійним, простим та розширюваним способом.

Коли ви намагаєтесь адаптувати деякий асинхронний потік даних у Rx (або створити асинхронний потік даних з того, який наразі не є асинхронним), як правило, найчастіші випадки:

  • Джерело даних - це подія : Як каже Лі, це найпростіший випадок: використовуйте FromEvent і прямуйте до паба.

  • Джерело даних відбувається з синхронної операції, і ви хочете оновлювати опитування (наприклад, виклик веб-сервісу або бази даних). У цьому випадку ви можете використовувати запропонований Лі підхід, або для простих випадків ви можете використовувати щось подібне Observable.Interval.Select(_ => <db fetch>). Ви можете використовувати DistinctUntilChanged () для запобігання публікації оновлень, коли нічого не змінилося у вихідних даних.

  • Джерело даних - це якась асинхронна програма, яка викликає зворотний дзвінок . У цьому випадку використовуйте Observable.Create, щоб підключити зворотний дзвінок, щоб викликати OnNext / OnError / OnComplete на спостерігача.

  • Джерелом даних є виклик, який блокує, поки не з’являться нові дані (наприклад, деякі операції зчитування синхронного сокета): У цьому випадку ви можете використовувати Observable.Create, щоб обернути імперативний код, який зчитується з сокета і публікується в Observer.OnNext при зчитуванні даних Це може бути схоже на те, що ви робите з Subject.

Використання Observable.Create vs створення класу, який керує Subject, досить еквівалентно використанню ключового слова дохідності проти створення цілого класу, який реалізує IEnumerator. Звичайно, ви можете написати IEnumerator, щоб він був таким же чистим і хорошим громадянином, як і код виходу, але який з них краще інкапсульований і відчуває себе більш охайним дизайном? Те саме стосується Observable.Create і керування предметами.

Observable.Create дає вам чітку схему для ледачих налаштувань та чистого сну. Як ви цього досягаєте, коли клас обгортає тему? Вам потрібен якийсь метод запуску ... як ви знаєте, коли його зателефонувати? Або ти просто завжди починаєш це, навіть коли ніхто не слухає? І коли ви закінчите, як змусити його перестати читати з сокета / опитувати базу даних тощо? У вас повинен бути якийсь метод зупинки, і ви все одно повинні мати доступ не тільки до IObservable, на який ви підписалися, але і до класу, який створив Subject в першу чергу.

Завдяки Observable.Create все це загорнуте в одне місце. Тіло Observable.Create не запускається, поки хтось не підписується, тому якщо ніхто не підписується, ви ніколи не використовуєте свій ресурс. І Observable.Create повертає одноразові, які можуть чисто закрити ваш ресурс / зворотній зв'язок тощо - це називається, коли спостерігач скасовує підписку. Час життя ресурсів, які ви використовуєте для створення Спостережуваного, чітко пов'язаний із життям самого Спостережуваного.


1
Дуже чітке пояснення Observable.Create. Дякую!
Еван Моран

1
У мене все ще є випадки, коли я використовую тему, коли об’єкт брокера виставляє спостережуване (скажімо, його просто змінне властивість). Різні компоненти будуть викликати брокера, повідомляючи, коли це властивість змінюється (при виклику методу), і цей метод робить OnNext. Споживачі підписуються. Я думаю, я б використовував предмет поведінки в цьому випадку, чи це підходить?
Френк Швітерман

1
Це залежить від ситуації. Хороший дизайн Rx має тенденцію до перетворення системи на асинхронну / реактивну архітектуру. Інтегрувати невеликі компоненти реактивного коду з системою, що є вкрай необхідною, може бути важко. Рішення допомоги в діапазоні полягає у використанні суб'єктів для перетворення імперативних дій (виклики функцій, набори властивостей) у спостережувані події. Тоді ви закінчуєте маленькими кишенями реактивного коду і жодним реальним "ага!" мить. Зміна дизайну на моделювання потоку даних та реагування на нього, як правило, дає кращий дизайн, але це всеохоплююча зміна і вимагає зміни настрою та командного входу.
Niall Connaughton

1
Я зазначу тут (як Rx недосвідчений), що: Використовуючи Subjects, ви можете увійти у світ Rx в рамках вирощеного імперативного додатку і повільно його трансформувати. Крім того, щоб отримати перший досвід .... і, звичайно, пізніше змінити свій код на те, яким він повинен був бути з самого початку (lol). Але для початку я думаю, що це може бути корисно для використання предметів.
Robetto

9

Цитований текст блоку в значній мірі пояснює, чому ви не повинні використовувати Subject<T>, але, кажучи простіше, ви поєднуєте функції спостерігача і спостережуваного, вводячи між собою якийсь стан (будь то інкапсуляція чи розширення).

Тут ви стикаєтеся з неприємностями; ці обов'язки повинні бути окремими та відрізнятися один від одного.

Це означає, що у вашому конкретному випадку я рекомендую вам розбити свої проблеми на більш дрібні частини.

По-перше, у вас є гаряча нитка, і ви завжди контролюєте апаратне забезпечення сигналів для отримання сповіщень. Як би ти це зробив нормально? Події . Тож почнемо з цього.

Давайте визначимо, EventArgsщо ваша подія буде відкрито

// The event args that has the information.
public class BaseFrameEventArgs : EventArgs
{
    public BaseFrameEventArgs(IBaseFrame baseFrame)
    {
        // Validate parameters.
        if (baseFrame == null) throw new ArgumentNullException("IBaseFrame");

        // Set values.
        BaseFrame = baseFrame;
    }

    // Poor man's immutability.
    public IBaseFrame BaseFrame { get; private set; }
}

Тепер клас, який розпалює подію. Зверніть увагу, що це може бути статичний клас (так як ви завжди є потік , який працює моніторинг апаратного буфера), або те , що ви дзвоните на вимогу , який підписується на що . Вам доведеться змінити це за необхідності.

public class BaseFrameMonitor
{
    // You want to make this access thread safe
    public event EventHandler<BaseFrameEventArgs> HardwareEvent;

    public BaseFrameMonitor()
    {
        // Create/subscribe to your thread that
        // drains hardware signals.
    }
}

Тож тепер у вас є клас, який викриває подію. Спостерігачі добре працюють з подіями. Настільки, що існує першокласна підтримка перетворення потоків подій (подумайте про потік подій як про кілька запусків події) у IObservable<T>реалізацію, якщо ви дотримуєтесь стандартного шаблону подій, шляхом статичного FromEventPatternметоду в Observableкласі .

За допомогою джерела ваших подій та FromEventPatternметоду ми можемо IObservable<EventPattern<BaseFrameEventArgs>>легко створити ( EventPattern<TEventArgs>клас втілює те, що ви побачили у події .NET, зокрема екземпляр, похідний від EventArgsта об’єкт, що представляє відправника), наприклад:

// The event source.
// Or you might not need this if your class is static and exposes
// the event as a static event.
var source = new BaseFrameMonitor();

// Create the observable.  It's going to be hot
// as the events are hot.
IObservable<EventPattern<BaseFrameEventArgs>> observable = Observable.
    FromEventPattern<BaseFrameEventArgs>(
        h => source.HardwareEvent += h,
        h => source.HardwareEvent -= h);

Звичайно, ви хочете IObservable<IBaseFrame>, але це легко, використовуючи Selectметод розширення в Observableкласі, щоб створити проекцію (так само, як і в LINQ, і ми можемо все це перетворити в простий у використанні метод):

public IObservable<IBaseFrame> CreateHardwareObservable()
{
    // The event source.
    // Or you might not need this if your class is static and exposes
    // the event as a static event.
    var source = new BaseFrameMonitor();

    // Create the observable.  It's going to be hot
    // as the events are hot.
    IObservable<EventPattern<BaseFrameEventArgs>> observable = Observable.
        FromEventPattern<BaseFrameEventArgs>(
            h => source.HardwareEvent += h,
            h => source.HardwareEvent -= h);

    // Return the observable, but projected.
    return observable.Select(i => i.EventArgs.BaseFrame);
}

7
Дякую за вашу відповідь @casperOne, це був мій початковий підхід, але я вважаю, що "неправильно" додати подію просто для того, щоб я міг завершити її з Rx. В даний час я використовую делегатів (і так, я знаю, що саме це подія!), Щоб відповідати коду, який використовується для завантаження та збереження конфігурації, це повинно бути в змозі відновити складові конвеєри, і делегатська система дала мені найбільше гнучкість. Rx дає мені головний біль у цій галузі зараз, але сила всього іншого в рамках робить рішення проблеми конфігурації дуже вартим.
Ентоні

@Anthony Якщо ви можете змусити його зразок коду працювати, чудово, але, як я вже коментував, це не має сенсу. Щодо почуття "неправильного", я не знаю, чому поділ речей на логічні частини здається "неправильним", але ви не вказали достатньо деталей у своєму початковому дописі, щоб вказати, як найкраще перекласти це так, щоб IObservable<T>не було інформації про те, як ви " в даний час подається сигналізація з цією інформацією.
casperOne

@casperOne На вашу думку, чи буде використання предметів доречним для агрегатора повідомлень шин / подій?
kitsune

1
@kitsune Ні, я не розумію, чому б це зробити. Якщо ви думаєте про "оптимізацію", вам слід задати питання, чи це не проблема, чи оцінювали ви Rx причиною проблеми?
casperOne

2
Я згоден тут з casperOne, що розділяти проблеми - це гарна ідея. Я хотів би зазначити, що якщо ви перейдете з обладнанням Hardware to Event до Rx, ви втратите семантику помилок. Будь-які втрачені зв’язки або сеанси тощо не будуть піддаватися споживачеві. Тепер споживач не може вирішити, чи хочуть вони повторити, відключити, підписатися на іншу послідовність чи щось інше.
Лі Кемпбелл

0

Погано узагальнювати те, що Суб'єкти недобре використовувати для публічного інтерфейсу. Хоча це, безумовно, правда, що це не такий спосіб, яким повинен виглядати підхід до реактивного програмування, це, безумовно, хороший варіант покращення / рефакторингу для вашого класичного коду.

Якщо у вас є звичайна власність з публічним набором доступу та ви хочете сповіщати про зміни, немає нічого проти того, щоб замінити її на BehaviorSubject. INPC або додаткові інші події просто не такі чисті, і це мене особисто змушує. Для цього ви можете та повинні використовувати BehaviorSubjects як загальнодоступні властивості замість звичайних властивостей та виривати INPC або інші події.

Крім того, Subject-інтерфейс робить користувачів вашого інтерфейсу більш обізнаними про функціональність ваших властивостей і, швидше за все, підписуються, а не просто отримують значення.

Це найкраще використовувати, якщо ви хочете, щоб інші слухали / підписувались на зміни властивості.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.