【问题标题】:Reactive extensions delayed initialization反应式扩展延迟初始化
【发布时间】:2020-06-04 18:25:24
【问题描述】:

已经相当确定,在 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 的角度来看,上述做法是不好的做法。好的,所以我们需要在 SimpleInjector 完成后调用某种Initialize() 方法。但是我们的重播语义和我们的StartWith() 呢?我们的消费者在Subscribe 时期望一个值(现在假设这保证会在初始化后发生)!

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

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

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

【问题讨论】:

  • SomeTransform(); 在构造函数期间不会被调用。当有人实际使用MyObservable 属性时使用它。
  • 我对 Reactive Extensions 不是很熟悉,但是 SomeTransform() 实际上会在构造函数中被调用,还是只有在其他服务推送一个值时才会发生这种情况?

标签: c# system.reactive simple-injector


【解决方案1】:

想到一个简单的解决方案是使用Lazy&lt;T&gt;

这可能看起来像:

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;

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

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

请注意,Lazy&lt;T&gt; 的 ctor 有几个重载,如果您可能遇到多线程问题,可以使用这些重载。

【讨论】:

  • 谢谢@Ric,这也是一个很好的建议。
【解决方案2】:

注入构造函数应该是simplereliable。这意味着不赞成以下做法:

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

考虑到响应式扩展的工作方式,您的 MyType 构造函数似乎没有执行任何 I/O。在创建MyType 期间,不会调用其SomeTransform 方法。相反,observable 配置为在推送对象时调用SomeTransform。这意味着从 DI 的角度来看,您的注入仍然“简单”且快速。有时您的类需要在存储传入依赖项之上进行一些初始化。例如,创建和存储Lazy&lt;T&gt; 就是一个很好的例子。它允许延迟执行一些 I/O,同时仍然拥有比仅仅“接收依赖项”更多的代码。

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

所以,严格来说,从 DI 的角度来看,这种配置可能很麻烦。但是在使用 Reactive Extensions 时很难想象一个不同的模型。这意味着您必须将 observables 的这种配置移出构造函数,并在构建对象图之后执行此操作。但这可能会导致必须打开您的类,以便Composition Root 可以访问需要调用的方法。它还会导致Temporal Coupling

换句话说,当使用响应式扩展时,最好有一些设计规则来防止麻烦。这些规则可能是:

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

【讨论】:

  • (1) 在我自己考虑了一段时间之后,我完全同意你对此的看法。我认为我最初设置的主要问题是StartWith()。这本身会导致SomeTransform() 在构建过程中运行,这很糟糕。我得出的结论是,当将 DI 与 RX 结合使用时,如果初始值需要计算,我们永远不应该创建具有 Replay 语义的 observable。
  • (2) 此外,如果构造函数中创建的 observable 没有在构造函数中订阅也更好。订阅的行为本身可能会触发计算,因为消费的流可能会立即发出一个值,而我们不能在构造函数中发生这种情况。
猜你喜欢
  • 2020-05-10
  • 1970-01-01
  • 2011-11-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-10-08
  • 2017-11-11
  • 1970-01-01
相关资源
最近更新 更多