响应式扩展延迟初始化
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 的同时很好地绕过这些限制?以下是要求摘要:
- 不要在 ctor 中做大量工作(即
SomeTransform
)不应该 运行
_myObservable
应该是 readonly
MyObservable
应该表现出 Replay(1)
语义
- 我们应该始终有一个初始值(因此
StartWith
)
- 我们不想在
MyType
中 Subscribe
并缓存值(我们喜欢不变性)
我尝试创建一个额外的 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
(您已经拥有从 MyType
到 IService
的设计时依赖)。这与在 .NET 中处理事件非常相似。这样做的结果是它可能导致 MyType
被 IService
保持存活,即使 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>
属性在其类型构造后应始终完全初始化并可用。
- 所有观察者和可观察对象都应该有相同的生命周期。
众所周知,在 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 的同时很好地绕过这些限制?以下是要求摘要:
- 不要在 ctor 中做大量工作(即
SomeTransform
)不应该 运行 _myObservable
应该是readonly
MyObservable
应该表现出Replay(1)
语义- 我们应该始终有一个初始值(因此
StartWith
) - 我们不想在
MyType
中Subscribe
并缓存值(我们喜欢不变性)
我尝试创建一个额外的 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
(您已经拥有从 MyType
到 IService
的设计时依赖)。这与在 .NET 中处理事件非常相似。这样做的结果是它可能导致 MyType
被 IService
保持存活,即使 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>
属性在其类型构造后应始终完全初始化并可用。 - 所有观察者和可观察对象都应该有相同的生命周期。