C# 当缓存(单例)依赖于计时器(瞬态)时,captive Dependencendy正常吗

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注册了这两个

TL;DR:我有一个依赖计时器的缓存。缓存是单例的,计时器必须是瞬态的(否则,需要计时器的不同组件将共享同一个计时器)。这是一个俘虏依赖,这是一个坏迹象™. 那么,这个设计有什么问题?在我看来,一个单一组件a应该能够使用另一个组件Z,该组件必须为每个组件aBC。。。那要看情况了


我正在开发一个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();