响应式扩展延迟初始化

Reactive extensions delayed initialization

众所周知,在 ctors 中为使用 SimpleInjector 解析的类型工作是不好的做法。尽管这通常会导致此类类型的某些延迟初始化,但一个特别有趣的案例是 Reactive Extensions 订阅。

例如,一个可观察到的序列表现出 Replay(1) 语义(如果我们考虑 StartWith,实际上是 BehaviorSubject),例如

private readonly IObservable<Value> _myObservable;

public MyType(IService service)
{
    _myObservable = service.OtherObservable
        .StartWith(service.Value)
        .Select(x => SomeTransform())
        .Replay(1)
        .RefCount();
}

public IObservable<Value> MyObservable => _myObservable;

现在假设 SomeTransform 的计算量很大。从 SimpleInjector 的角度来看,以上是不好的做法。好的,所以我们需要某种 Initialize() 方法在 SimpleInjector 完成后调用。但是我们的重放语义和我们的 StartWith() 呢?我们的消费者在 Subscribe 时期望一个值(现在假设这保证在初始化后发生)!

我们如何在满足 SimpleInjector 的同时很好地绕过这些限制?以下是要求摘要:

  1. 不要在 ctor 中做大量工作(即 SomeTransform)不应该 运行
  2. _myObservable 应该是 readonly
  3. MyObservable 应该表现出 Replay(1) 语义
  4. 我们应该始终有一个初始值(因此 StartWith
  5. 我们不想在 MyTypeSubscribe 并缓存值(我们喜欢不变性)

我尝试创建一个额外的 Observable,它以 false 开头,然后在初始化时设置为 true,然后将其与 _myObservable 合并,但不能完全让它工作。此外,它似乎不是最佳解决方案。本质上,我想做的就是延迟到 Initialize() 完成。一定有某种我没有看到的方法可以做到这一点?

想到的一个简单的解决方案是使用 Lazy<T>

这可能看起来像:

private readonly Lazy<IObservable<Value>> _lazyMyObservable;

public MyType(IService service)
{
    _lazyMyObservable =  new Lazy<IObservable<Value>>(() => this.InitObservable(service));
}

private IObservable<Value> InitObservable(IService service)
{
    return service.OtherObservable
        .StartWith(service.Value)
        .Select(x => SomeTransform())
        .Replay(1)
        .RefCount();
 }

 public IObservable<Value> MyObservable => _lazyMyObservable.Value;

这将在不实际调用 SomeTransform() 的情况下初始化变量 _lazyMyObservable。当消费者请求 MyType.MyObservable 时,InitObservable 代码将被调用一次且仅调用一次。这将初始化推迟到实际使用代码的地方。

这将使您的构造函数保持干净整洁,并且无需添加初始化逻辑。

请注意,Lazy<T> 的构造函数有多个重载,如果您可能遇到多线程问题,可以使用这些重载。

注入构造函数应该是simple and reliable。这意味着以下做法是不受欢迎的:

  • 在构造函数中执行任何 I/O 操作。 I/O 操作可能会失败并使对象图的构造不可靠。
  • 在构造函数中使用 class 的依赖项。不仅被调用的依赖项会导致 I/O 自身,有时注入的依赖项还没有(尚未)完全初始化,最终初始化发生在稍后的时间点。也许在构建对象图之后。

考虑到 Reactive Extensions 的工作原理,您的 MyType 构造函数似乎没有做任何事情 I/O。它的 SomeTransform 方法在创建 MyType 期间不会被调用。相反,observable 被配置为在推送对象时调用 SomeTransform。这意味着从 DI 的角度来看,您的注入仍然 'simple' 并且速度很快。有时您的 classes 需要在存储传入依赖项之上进行一些初始化。例如,创建和存储 Lazy<T> 就是一个很好的例子。它允许延迟做一些 I/O,同时仍然有比仅仅 "receiving the dependencies."

更多的代码

但是您仍然在构造函数中访问依赖项,如果该依赖项或其依赖项未完全初始化,这可能会导致问题。此外,使用 Reactive Extensions,您可以将运行时依赖从 IService 返回到 MyType(您已经拥有从 MyTypeIService 的设计时依赖)。这与在 .NET 中处理事件非常相似。这样做的结果是它可能导致 MyTypeIService 保持存活,即使 MyType 寿命预期更短。

因此,严格来说,从 DI 的角度来看,这种配置可能会很麻烦。但是很难想象在使用 Reactive Extensions 时会有不同的模型。这意味着您必须将可观察对象的这种配置从构造函数中移出,并在构建对象图之后进行。但这可能会导致不得不打开你的 classes 所以 Composition Root has access to the methods that need to be called. It also causes Temporal Coupling.

换句话说,在使用 Reactive Extensions 时,最好有一些设计规则来防止麻烦。这些规则可以是:

  • 所有公开的 IObservable<T> 属性在其类型构造后应始终完全初始化并可用。
  • 所有观察者和可观察对象都应该有相同的生命周期。