使用Subject来解耦Observable订阅和初始化

时间:2013-05-07 07:17:13

标签: c# system.reactive subject

我有一个公开IObservable状态的API。但是这种状态取决于潜在的可观察源,必须通过Init进行初始化。

我想做的是保护用户不必按正确的顺序执行操作:如果他们在执行Status之前尝试订阅Init,则目前就是这样,他们得到一个例外,因为他们的来源没有被初始化。

所以我有一个天才的想法:使用Subject来解耦这两个:订阅我的Status的外部用户只是订阅主题,然后当他们调用Init时,我使用我的主题订阅了基础服务。

代码中的想法

private ISubject<bool> _StatusSubject = new Subject<bool>();
public IObservable<bool> Status { get { return _StatusSubject; } }

public void Init() 
{
    _Connection = new Connection();
    Underlying.GetDeferredObservable(_Connection).Subscribe(_StatusSubject);
}

但是,从虚拟项目的测试来看,问题在于初始化 即使没有人订阅该主题,也可以通过订阅主题来“唤醒”我的基础Observable。如果可能的话,这是我想避免的,但我不确定如何......

(我也注意到the received wisdom“一般规则是,如果你使用某个主题,那么你做错了什么”)

3 个答案:

答案 0 :(得分:6)

似乎您缺少的概念是如何知道某人何时开始倾听并且仅启动您的潜在来源。通常您使用Observable.Create或其中一个兄弟(DeferUsing,...)来执行此操作。

如果没有Subject

,请执行以下操作:
private IObservable<bool> _status = Observable.Defer(() =>
{
    _Connection = new Connection();
    return Underlying.GetDeferredObservable(_Connection);
};

public IObservable<bool> Status { get { return _status; } }

Defer在实际订阅之前不会调用init代码。

但这有几个潜在的问题:

  1. 每个观察者都会建立新连接
  2. 当观察员取消订阅时,连接未被清除。
  3. 第二个问题很容易解决,所以让我们先做。假设您的Connection是一次性的,在这种情况下您可以这样做:

    private IObservable<bool> _status = Observable
        .Using(() => new Connection(),
               connection => Underlying.GetDeferredObservable(connection));
    
    public IObservable<bool> Status { get { return _status; } }
    

    通过此迭代,每当有人订阅时,都会创建一个新的Connection并传递给第二个lamba方法来构造observable。每当观察者取消订阅时,ConnectionDisposed。如果Connection不是IDisposable,那么您可以使用Disposable.Create(Action)创建IDisposable,它将运行您需要运行的任何操作来清理连接。

    您仍然遇到每个观察者创建新连接的问题。我们可以使用PublishRefCount来解决该问题:

    private IObservable<bool> _status = Observable
        .Using(() => new Connection(),
               connection => Underlying.GetDeferredObservable(connection))
        .Publish()
        .RefCount();
    
    public IObservable<bool> Status { get { return _status; } }
    

    现在,当第一个观察者订阅时,将创建连接并订阅基础observable。后续观察员将共享该连接并将获取当前状态。当 last 观察者取消订阅时,将关闭连接并关闭所有内容。如果另一个观察者在此之后订阅,则所有观察者都会重新开始。

    在幕后,Publish实际上是使用Subject来共享单个可观察来源。并且RefCount正在跟踪目前正在观察的观察者数量。

答案 1 :(得分:1)

我可能会在这里过于简单化,但让我按照要求使用Subject

您的Thingy

public class Thingy
{
    private BehaviorSubject<bool> _statusSubject = new BehaviorSubject<bool>(false);    
    public IObservable<bool> Status
    {
        get
        {
            return _statusSubject;
        }
    }

    public void Init()
    {
        var c = new object();
        new Underlying().GetDeferredObservable(c).Subscribe(_statusSubject);
    }
}

假冒Underlying

public class Underlying
{
    public IObservable<bool> GetDeferredObservable(object connection)
    {
        return Observable.DeferAsync<bool>(token => {
            return Task.Factory.StartNew(() => {
                Console.WriteLine("UNDERLYING ENGAGED");
                Thread.Sleep(1000);
                // Let's pretend there's some static on the line...
                return Observable.Return(true)
                    .Concat(Observable.Return(false))
                    .Concat(Observable.Return(true));
            }, token);
        });
    }
}

安全带:

void Main()
{
    var thingy = new Thingy();
    using(thingy.Status.Subscribe(stat => Console.WriteLine("Status:{0}", stat)))
    {
        Console.WriteLine("Waiting three seconds to Init...");
        Thread.Sleep(3000);
        thingy.Init();
        Console.ReadLine();
    }
}

输出:

Status:False
Waiting three seconds to Init...
UNDERLYING ENGAGED
Status:True
Status:False
Status:True

答案 2 :(得分:0)

嗯,玩过这个,我不认为我只能用一个主题来做。

尚未完成测试/尝试,但这是我目前提出的似乎有效的方法,但它并不能保护我免受主题的影响,因为我还在内部使用它。

public class ObservableRouter<T> : IObservable<T>
{
    ISubject<T> _Subject = new Subject<T>();
    Dictionary<IObserver<T>, IDisposable> _ObserverSubscriptions 
                               = new Dictionary<IObserver<T>, IDisposable>();
    IObservable<T> _ObservableSource;
    IDisposable _SourceSubscription;

    //Note that this can happen before or after SetSource
    public IDisposable Subscribe(IObserver<T> observer)
    {
        _ObserverSubscriptions.Add(observer, _Subject.Subscribe(observer));
        IfReadySubscribeToSource();
        return Disposable.Create(() => UnsubscribeObserver(observer));
    }

    //Note that this can happen before or after Subscribe
    public void SetSource(IObservable<T> observable)
    {
        if(_ObserverSubscriptions.Count > 0 && _ObservableSource != null) 
                  throw new InvalidOperationException("Already routed!");
        _ObservableSource = observable;
        IfReadySubscribeToSource();
    }

    private void IfReadySubscribeToSource()
    {
        if(_SourceSubscription == null &&
           _ObservableSource != null && 
           _ObserverSubscriptions.Count > 0)
        {
            _SourceSubscription = _ObservableSource.Subscribe(_Subject);
        }
    }

    private void UnsubscribeObserver(IObserver<T> observer)
    {
        _ObserverSubscriptions[observer].Dispose();
        _ObserverSubscriptions.Remove(observer);
        if(_ObserverSubscriptions.Count == 0)
        {
            _SourceSubscription.Dispose();
            _SourceSubscription = null;
        }
    }
}