在IConnectableObservable中包装旧对象

时间:2013-11-22 13:55:28

标签: c#-4.0 system.reactive reactive-programming

我有一个遗留的基于事件的对象,看起来非常适合RX:在连接到网络源之后,它会在收到消息时引发事件,并且可能因单个错误而终止(连接死亡等) 。)或(很少)表示不再有消息。此对象还有一些预测 - 大多数用户只对收到的消息的子集感兴趣,因此只有在众所周知的消息子类型出现时才会引发备用事件。

因此,在学习更多关于反应式编程的过程中,我构建了以下包装器:

class LegacyReactiveWrapper : IConnectableObservable<TopLevelMessage>
{
    private LegacyType _Legacy;
    private IConnectableObservable<TopLevelMessage> _Impl;
    public LegacyReactiveWrapper(LegacyType t) 
    {
        _Legacy = t;
        var observable = Observable.Create<TopLevelMessage>((observer) => 
        {
            LegacyTopLevelMessageHandler tlmHandler = (sender, tlm) => observer.OnNext(tlm);
            LegacyErrorHandler errHandler = (sender, err) => observer.OnError(new ApplicationException(err.Message));
            LegacyCompleteHandler doneHandler = (sender) => observer.OnCompleted();

            _Legacy.TopLevelMessage += tlmHandler;
            _Legacy.Error += errHandler;
            _Legacy.Complete += doneHandler;

            return Disposable.Create(() => 
            {
                _Legacy.TopLevelMessage -= tlmHandler;
                _Legacy.Error -= errHandler;
                _Legacy.Complete -= doneHandler;
            });
        });

        _Impl = observable.Publish();
    }

    public IDisposable Subscribe(IObserver<TopLevelMessage> observer)
    {
        return _Impl.RefCount().Subscribe(observer);
    }

    public IDisposable Connect()
    {
        _Legacy.ConnectToMessageSource();
        return Disposable.Create(() => _Legacy.DisconnectFromMessageSource());
    }

    public IObservable<SubMessageA> MessageA
    {
        get
        {
            // This is the moral equivalent of the projection behavior
            // that already exists in the legacy type. We don't hook
            // the LegacyType.MessageA event directly.
            return _Impl.RefCount()
                    .Where((tlm) => tlm.MessageType == MessageType.MessageA)
                    .Select((tlm) => tlm.SubMessageA);
        }
    }

    public IObservable<SubMessageB> MessageB
    {
        get
        {
            return _Impl.RefCount()
                    .Where((tlm) => tlm.MessageType == MessageType.MessageB)
                    .Select((tlm) => tlm.SubMessageB);
        }
    }
}

关于它如何在其他地方使用的东西感觉......不管怎样......不过。这是示例用法,虽然有效,但感觉很奇怪。我的测试应用程序的UI上下文是WinForms,但它并不重要。

// in Program.Main... 

MainForm frm = new MainForm();

// Updates the UI based on a stream of SubMessageA's
IObserver<SubMessageA> uiManager = new MainFormUiManager(frm);

LegacyType lt = new LegacyType();
// ... setup lt...

var w = new LegacyReactiveWrapper(lt);

var uiUpdateSubscription = (from msgA in w.MessageA
                            where SomeCondition(msgA)
                            select msgA).ObserveOn(frm).Subscribe(uiManager);

var nonUiSubscription = (from msgB in w.MessageB
                         where msgB.SubType == MessageBType.SomeSubType
                         select msgB).Subscribe(
                             m => Console.WriteLine("Got MsgB: {0}", m),
                             ex => Console.WriteLine("MsgB error: {0}", ex.Message),
                             () => Console.WriteLine("MsgB complete")
                         );

IDisposable unsubscribeAtExit = null;
frm.Load += (sender, e) => 
{
    var connectionSubscription = w.Connect();
    unsubscribeAtExit = new CompositeDisposable(
                               uiUpdateSubscription,
                               nonUiSubscription,
                               connectionSubscription);
};

frm.FormClosing += (sender, e) => 
{
    if(unsubscribeAtExit != null) { unsubscribeAtExit.Dispose(); }
};


Application.Run(frm);

此工作 - 表单启动,UI更新,当我关闭它时,订阅被清理并且进程退出(如果LegacyType的网络连接仍然连接,它将不会执行)。严格来说,仅仅处理connectionSubscription就足够了。但是,明确的Connect对我来说很奇怪。由于RefCount应该为您执行此操作,因此我尝试修改包装器,以便在_Impl.RefCountMessageA中使用MessageB并在以后显式连接时使用{this.RefCount 1}}而是将对Subscribe的调用移动到Load处理程序。这有一个不同的问题 - 第二个订阅触发了对LegacyReactiveWrapper.Connect的另一个调用。我认为Publish / RefCount背后的想法是“先进先出触发连接,最后一次配置连接。”

我想我有三个问题:

  1. 我是否从根本上误解了Publish / RefCount
  2. 是否有一些首选方法可以实现IConnectableObservable<T>,但不涉及授权通过IObservable<T>.Publish获得的方式?我知道你不应该自己实现IObservable<T>,但我不明白如何将连接逻辑注入IConnectableObservable<T>给你的Observable.Create().Publish()Connect应该是幂等的吗?
  3. 更熟悉RX /反应式编程的人会看样本是否使用了包装器,并说“那是丑陋和破碎的”或者这不像看起来那么奇怪吗?

2 个答案:

答案 0 :(得分:2)

我不确定您是否需要直接公开Connect。我将简化如下,使用Publish().RefCount()作为封装的实现细节;这将导致仅在需要时进行传统连接。这里第一个用户导致连接,最后一个用户导致断开连接。另请注意,这正确地在所有订阅者之间共享一个RefCount,而您的实现使用每个消息类型RefCount,这可能不是预期的。用户无需明确连接:

public class LegacyReactiveWrapper
{
    private IObservable<TopLevelMessage> _legacyRx; 

    public LegacyReactiveWrapper(LegacyType legacy)
    {
        _legacyRx = WrapLegacy(legacy).Publish().RefCount();
    }

    private static IObservable<TopLevelMessage> WrapLegacy(LegacyType legacy)
    {
        return Observable.Create<TopLevelMessage>(observer =>
        {
            LegacyTopLevelMessageHandler tlmHandler = (sender, tlm) => observer.OnNext(tlm);
            LegacyErrorHandler errHandler = (sender, err) => observer.OnError(new ApplicationException(err.Message));
            LegacyCompleteHandler doneHandler = sender => observer.OnCompleted();

            legacy.TopLevelMessage += tlmHandler;
            legacy.Error += errHandler;
            legacy.Complete += doneHandler;
            legacy.ConnectToMessageSource();

            return Disposable.Create(() =>
            {
                legacy.DisconnectFromMessageSource();
                legacy.TopLevelMessage -= tlmHandler;
                legacy.Error -= errHandler;
                legacy.Complete -= doneHandler;
            });
        });
    }

    public IObservable<TopLevelMessage> TopLevelMessage
    {
        get
        {
            return _legacyRx;
        }
    }

    public IObservable<SubMessageA> MessageA
    {
        get
        {
            return _legacyRx.Where(tlm => tlm.MessageType == MessageType.MessageA)
                            .Select(tlm => tlm.SubMessageA);
        }
    }

    public IObservable<SubMessageB> MessageB
    {
        get
        {
            return _legacyRx.Where(tlm => tlm.MessageType == MessageType.MessageB)
                            .Select(tlm => tlm.SubMessageB);
        }
    }
}

另一个观察是Publish().RefCount()将在订阅者数量达到0时删除基础订阅。通常,当我需要维护订阅时,即使订阅者数量很多,我也只使用Connect公布的来源降至零(稍后可能会再次恢复)。虽然很少需要这样做 - 只有当你可能不需要连接比保留订阅资源更昂贵时才会这样做。

答案 1 :(得分:1)

  1. 您的理解并非完全错误,但您似乎确实存在一些误解。

    您似乎认为在同一个源IObservable上多次调用RefCount将导致共享引用计数。他们不;每个实例都有自己的计数。因此,您正在对_Impl进行多次订阅,每次调用一次订阅或调用Message属性。

    您也可能期望_Impl以及IConnectableObservable以某种方式调用您的Connect方法(因为您似乎很惊讶您需要在消费代码中调用Connect)。所有Publish都会导致发布对象的订阅者(从.Publish()调用返回)共享对底层源可观察对象的单个订阅(在这种情况下,通过调用Observable.Create创建的对象)

    通常情况下,我会立即一起使用Publish和RefCount(例如source.Publish().RefCount())以获得上述共享订阅效果,或者在不需要调用Connect以开始订阅原始源的情况下进行冷可观察热。但是,这依赖于使用.Publish()。RefCount()为所有订阅者返回的相同对象(如上所述)。

  2. 您对Connect的实施似乎是合理的。我不知道有关Connect是否应该是幂等的任何建议,但我个人不会期望它。如果你想要它,你只需要跟踪它的回调处理它的返回值,以获得正确的平衡。

    我认为您不需要像现在这样使用Publish,除非有理由避免将多个事件处理程序附加到旧对象。如果您确实需要避免这种情况,我建议您将_Impl更改为普通IObservable,并使用Publish关注RefCount

  3. 您的MessageAMessageB属性可能会成为用户混淆的根源,因为它们返回IObservable,但仍需要在基础对象上调用Connect才能开始接收消息。我要么将它们更改为IConnectableObservables,它们以某种方式委托给原始的Connect(此时幂等性讨论变得更加相关)或删除属性,让用户在需要时自己制作(相当简单的)投影。

    < / LI>