C# 当缓存(单例)依赖于计时器(瞬态)时,captive Dependencendy正常吗
TL;DR:我有一个依赖计时器的缓存。缓存是单例的,计时器必须是瞬态的(否则,需要计时器的不同组件将共享同一个计时器)。这是一个俘虏依赖,这是一个坏迹象™. 那么,这个设计有什么问题?在我看来,一个单一组件a应该能够使用另一个组件Z,该组件必须为每个组件a,B,C。。。那要看情况了C# 当缓存(单例)依赖于计时器(瞬态)时,captive Dependencendy正常吗,c#,dependency-injection,simple-injector,C#,Dependency Injection,Simple Injector,TL;DR:我有一个依赖计时器的缓存。缓存是单例的,计时器必须是瞬态的(否则,需要计时器的不同组件将共享同一个计时器)。这是一个俘虏依赖,这是一个坏迹象™. 那么,这个设计有什么问题?在我看来,一个单一组件a应该能够使用另一个组件Z,该组件必须为每个组件a,B,C。。。那要看情况了 我正在开发一个ASP.NET web API,用于提供聚合销售数据。在我的后端,我有一个由cachingordrepository装饰的OrderRepository。我使用SimpleInjector注册了这两个
我正在开发一个ASP.NET web API,用于提供聚合销售数据。在我的后端,我有一个由
cachingordrepository
装饰的OrderRepository。我使用SimpleInjector注册了这两个函数。在我看来,这两个都应该注册为单例(singleton)似乎是合乎逻辑的——缓存至少必须注册为单例(否则,可能会为每个web请求实例化一个新的/空的缓存,从而破坏缓存的用途)
CachingOrderRepository
通过每晚自动更新自身(将所有订单保存在内存中)来工作,因此依赖于ITimer
(用于System.Timers.Timer
的可模拟包装)。我对长寿命计时器没有任何问题(根据设计,这个特定的计时器实际上是一个单例计时器),但它需要注册为一个临时依赖项,因为每个需要计时器的组件都需要一个唯一的实例
但是,由于缓存注册为单例,计时器注册为瞬态,因此这是一个俘虏依赖项,并在SimpleInjector中发出生活方式不匹配警告,他们说:
何时忽略警告
不要忽略这些警告。此警告的误报很少见,即使发生,注册或应用程序设计也可以随时以警告消失的方式进行更改
我认为错误在于我的设计,这不是罕见的误报。我的问题是:我的设计有什么问题?
我意识到我可以通过使缓存依赖于ITimer
的抽象工厂来规避警告,但这似乎正是一种规避警告的方法,将ITimer
实例的构造从SimpleInjector移动到显式工厂,为了单元测试cachingordrepository
(我需要注入一个返回计时器模拟的工厂模拟),需要模拟另一层抽象。如果您建议使用抽象工厂,请解释为什么这不仅仅是规避警告的一种方式(在这种特殊情况下,它会让父组件控制计时器的处理,但一般来说,工厂返回的组件可能不是一次性的,因此这不会是一个问题)
(注意:这不是一个捕获依赖通常有什么问题的问题,其他地方也有很好的答案。这是一个关于上述特定设计有什么问题的问题。但是,如果我似乎误解了捕获依赖的某些内容,请随时澄清。)
更新:根据要求,以下是缓存和计时器的实现:
public class CachingOrderRepository : IOrderRepository
{
private readonly IOrderRepository repository;
private ITimer timer;
private Order[] cachedOrders;
private bool isPrimed;
public CachingEntityReader(IOrderRepository repository, ITimer timer)
{
this.repository = repository;
this.timer = timer;
this.StartTimer();
}
private void StartTimer()
{
this.timer.Elapsed += this.UpdateCache;
this.timer.Interval = TimeSpan.FromHours(24);
this.timer.Start();
}
private void UpdateCache()
{
Order[] orders = this.repository.GetAll().ToArray();
this.cachedOrders = orders;
this.isPrimed = true;
}
public IEnumerable<Order> GetAllValid()
{
while (!this.isPrimed)
{
Task.Delay(100);
}
return this.cachedOrders;
}
}
public class TimedEventRaiser : ITimedEventRaiser, IDisposable
{
private readonly Timer timer;
public TimedEventRaiser()
{
this.timer = new Timer();
this.timer.Elapsed += (_, __) => this.Elapsed?.Invoke();
}
public event Action Elapsed;
public TimeSpan Interval
{
get => TimeSpan.FromMilliseconds(this.timer.Interval);
set => this.timer.Interval = value.TotalMilliseconds;
}
public void Start()
{
this.timer.Start();
}
public void Stop()
{
this.timer.Stop();
}
public void Dispose()
{
this.timer.Dispose();
}
}
公共类cachingordrepository:iordrepository
{
专用只读IOrderRepository存储库;
私人定时器;
私人订单[]缓存器;
私人住宅受到歧视;
公共CachingEntityReader(IOrderRepository存储库,ITimer计时器)
{
this.repository=存储库;
this.timer=计时器;
这个。StartTimer();
}
私有void StartTimer()
{
this.timer.appeased+=this.UpdateCache;
this.timer.Interval=TimeSpan.FromHours(24);
this.timer.Start();
}
私有void UpdateCache()
{
Order[]orders=this.repository.GetAll().ToArray();
this.cachedOrders=订单;
this.isPrimed=true;
}
公共IEnumerable GetAllValid()
{
而(!this.isPrimed)
{
任务延迟(100);
}
归还这个。缓存器;
}
}
公共类TimedEventRaiser:ITimedEventRaiser,IDisposable
{
专用只读定时器;
公共时间
{
this.timer=新计时器();
this.timer.appeased+=(u,_u)=>this.appeased?.Invoke();
}
公共事件行动;
公共时间间隔
{
get=>TimeSpan.FromMillistics(this.timer.Interval);
set=>this.timer.Interval=value.total毫秒;
}
公开作废开始()
{
this.timer.Start();
}
公共停车场()
{
this.timer.Stop();
}
公共空间处置()
{
this.timer.Dispose();
}
}
但它需要注册为一个临时依赖项,因为每个需要计时器的组件都需要一个唯一的实例
您描述的行为实际上是一种不同的生活方式,Simple Injector不支持开箱即用,即:。这样的生活方式是,但通常不建议的,因为,如前所述,设计通常可以调整以不需要使用状态依赖
因此,并不是说每个依赖项都有一个实例本身就不好,而是一般来说,使用无状态和不可变的组件可以产生更干净、更简单和更可维护的代码
例如,在您的情况下,您的代码将受益于从装饰器本身提取缓存的责任。这将简化decorator,并允许缓存实现也成为单例
以下示例显示了CachingOrderRepository
的简化版本:
public class CachingOrderRepository : IOrderRepository
{
private readonly IOrderRepository repository;
private readonly ICache cache;
private readonly ITimeProvider time;
public CachingOrderRepository(
IOrderRepository repository, ICache cache, ITimeProvider time)
{
this.repository = repository;
this.cache = cache;
this.time = time;
}
public IEnumerable<Order> GetAllValid() =>
this.cache.GetOrAdd(
key: "AllValidOrders",
cacheDuration: this.TillMidnight,
valueFactory: () => this.repository.GetAllValid().ToList().AsReadOnly());
private TimeSpan TillMidnight => this.Tomorrow - this.time.Now;
private DateTime Tomorrow => this.time.Now.Date.AddHours(24);
}
用于缓存库(如th)的适配器实现
public interface ICache
{
T GetOrAdd<T>(string key, TimeSpan cacheDuration, Func<T> valueFactory);
}
public sealed class MemoryCacheCacheAdapter : ICache
{
private static readonly ObjectCache Cache = MemoryCache.Default;
public T GetOrAdd<T>(string key, TimeSpan cacheDuration, Func<T> valueFactory)
{
object value = Cache[key];
if (value == null)
{
value = valueFactory();
Cache.Add(key, value, new CacheItemPolicy
{
AbsoluteExpiration = DateTimeOffset.UtcNow + cacheDuration
});
}
return (T)value;
}
}
// Start the operation 2 seconds after start-up.
Timer timer = new Timer(TimeSpan.FromSeconds(2).TotalMilliseconds);
timer.Elapsed += (_, __) =>
{
try
{
// Wrap operation in the appropriate scope (if required)
using (ThreadScopedLifestyle.BeginScope(container))
{
var repo = container.GetInstance<IOrderRepository>();
// This will trigger a cache reload in case the cache has timed out.
repo.GetAllValid();
}
}
catch (Exception ex)
{
// TODO: Log exception
}
// Will run again at 01:00 AM tomorrow to refresh the cache
timer.Interval = DateTime.Today.AddHours(25);
};
timer.Start();