相当确定的是,在使用 SimpleInjector 解析的类型的 ctors 中做工作是一种不好的做法。尽管这通常会导致此类类型的某些延迟初始化,但反应式扩展订阅是一个特别有趣的情况。
以一个表现出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
( - 我们不想
Subscribe
MyType
内部并缓存值(我们喜欢不变性(
我尝试创建一个额外的可观察量,该可观察量从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;
这将初始化变量_lazyMyObservable
而不实际调用SomeTransform()
。当消费者要求MyType.MyObservable
时,InitObservable
代码将被调用一次,并且只调用一次。这会将初始化推迟到实际使用代码的位置。
这将使您的构造函数保持美观和干净,并且无需添加初始化逻辑。
请注意,Lazy<T>
的 ctor 有几个重载,如果多线程处理出现问题,可以使用这些重载。
注入构造函数应该简单可靠。这意味着以下做法是不受欢迎的:
- 在构造函数内执行任何 I/O 操作。I/O 操作可能会失败,并使对象图的构造不可靠。
- 在构造函数中使用类的依赖项。被调用的依赖项不仅会导致其自身的 I/O,有时注入的依赖项尚未(尚未(完全初始化,最终初始化发生在稍后的时间点。也许在构建了对象图之后。
考虑到反应式扩展的工作方式,您的MyType
构造函数似乎不执行任何 I/O。它的SomeTransform
方法在创建MyType
期间不调用。相反,可观察量配置为在推送对象时调用SomeTransform
。这意味着从DI的角度来看,您的注射仍然是"简单"和快速的。有时,您的类需要在存储传入依赖项的基础上进行一些初始化。例如,创建和存储Lazy<T>
就是一个很好的例子。它允许延迟执行一些I/O,同时仍然拥有更多的代码,而不仅仅是"接收依赖项"。
但是您仍然在访问构造函数中的依赖项,如果该依赖项或其依赖项未完全初始化,则可能会导致麻烦。此外,使用反应式扩展,您可以创建从IService
到MyType
的运行时依赖项(您已经具有从MyType
到IService
的设计时依赖项(。这与在 .NET 中处理事件非常相似。这样做的后果是,即使MyType
MyType
的寿命预计会更短,IService
也会
使存活。因此,严格来说,从 DI 的角度来看,这种配置可能会很麻烦。但是在使用反应式扩展时很难想象不同的模型。这意味着您必须将可观察量的此配置移出构造函数,并在构建对象图之后执行此操作。但这可能会导致必须打开您的类,以便组合根可以访问需要调用的方法。它还会导致时间耦合。
换句话说,在使用反应式扩展时,最好有一些设计规则来防止出现问题。这些规则可以是:
- 所有公开的
IObservable<T>
属性都应始终完全初始化,并在其类型构造后可用。 - 所有观察器和可观察量应具有相同的生存期。