在不使用Singleton的情况下封装昂贵的资源

时间:2010-07-13 20:13:35

标签: c# caching singleton

我正在努力更新一个绝对充满Singleton类的遗留应用程序。一个完美的例子是SnmpConnector类:

public SnmpConnector
{
  public static IEnumerable<string> HostIpAddresses
  {
    ...
  }

  private static SnmpConnector instance;
  public static SnmpConnector Instance
  {
    if (instance == null)
      instance = new SnmpConnector();
    return instance;
  }

  private SnmpConnector()
  {
    foreach (string IpAddress in HostIpAddresses)
    {
      ...
    }
  }

  ...
}

此更新的目标是提高代码库的可测试性,因此我想摆脱单身人士。我已经抽象出SnmpConnector的数据源,要么从测试数据库获取数据,要么从查询实时服务器:

public interface ISnmpDataSource
{
  public DataTable MacTable
  {
    get;
    private set;
  }

  public DataTable PortTable
  {
    get;
    private set;
  }

  ...
}

public TestSnmpDataSource : ISnmpDataSource
{
  public FileInfo DataSource
  {
    get;
    private set;
  }

  ...
}

public SnmpDataSource : ISnmpDataSource
{
  public List<string> HostIpAddresses
  {
    get;
    private set;
  }

  ...
}

public SnmpConnector
{
  public SnmpConnector(ISnmpDataSource DataSource)
  {
    ...
  }

  ...
}

现在,我正在尝试测试这些组件并遇到可能导致SnmpConnector首先成为Singleton的问题:测试SnmpDataSource需要花费大量时间。事实证明,从实时交换机获取MAC表和端口表需要10到20秒之间的时间。我已经为这个特定的类编写了13个单元测试,因此完成这些测试需要两分钟。虽然这很令人讨厌,但是一旦这些更新发布到我们原始的代码库,它就会变得更糟。通过这种新的重构,没有什么能阻止程序员反复创建和丢弃SnmpDataSource。

现在,这些表中的数据基本上是静态的;旧的Singleton和新的SnmpDataSource都维护一个仅每四个小时更新一次的缓存。我是否必须将SnmpDataSource设为Singleton以防止出现此问题?

3 个答案:

答案 0 :(得分:3)

使用依赖注入,并将SnmpDataSource传递给任何需要它的东西,或者传入Func<SnmpDataSource>,这可以根据需要懒惰地创建实例。

你的目标是SnmpDataSource应该自行更新,或者呼叫者会在几小时后获得新版本吗?

答案 1 :(得分:0)

您可以尝试使用实现相同接口的缓存感知版本来包装/装饰SnmpDataSource,然后注入缓存感知版本。

*编辑 - 或者你可以做乔恩建议工厂Func进行缓存的地方(它将返回一个新实例或缓存版本,具体取决于创建最后一个实例的时间)。同样的事情,实现略有不同。乔恩的版本可能更有意义。

public CachedSnmpDataSource : ISnmpDataSource
{
  private DateTime m_lastRetrieved;
  private TimeSpan m_cacheExpiryPeriod;
  private List<string> m_hostIpAddresses;
  private Func<SnmpDataSource> m_dataSourceCreator

  public CachedSnmpDataSource(Func<SnmpDataSource> dataSourceCreator, TimeSpan cacheExpiryPeriod)
  {
      m_dataSourceCreator = dataSourceCreator;
      m_cacheExpiryPeriod = cacheExpiryPeriod;
  }

  public List<string> HostIpAddresses
  {
    get
    {
       if(!IsRecentCachedVersionAvailable()) 
       {
           CreateCachedVersion();
       }

       return new List<string>(m_hostIpAddresses);
    } 

    private bool IsRecentCachedVersionAvailable()
    {
        return m_hostIpAddresses != null && 
               (DateTime.Now - m_lastRetrieved) < m_cacheExpiryPeriod;
    }

    private void CreateCachedVersion()
    {  
       SnmpDataSource dataSource = m_dataSourceCreator();
       m_hostIpAddresses = dataSource.HostIpAddresses;
       m_lastRetrieved = DateTime.Now;       
    }
  }

  ...
}

答案 2 :(得分:0)

经过几次迭代,我最终找到了解决这个问题的巧妙方法。我将按原样留下接受的答案,但这是我最终使用的:

  1. ISnmpDataSource负责提取数据,如前所述。
  2. SnmpConnector知道只询问自己的缓存是否无效。
  3. 静态工厂类维护Dictionary<ISnmpDataSource, SnmpConnector>。基于此字典有一种static BuildSnmpConnector(ISnmpDataSource)方法。
  4. 现在使用该库看起来像这样:

    IEnumerable<string> IpAddresses = ...;
    string SqlConString = @"...";
    ISnmpDataSource Switches = new SnmpDataSource(IpAddresses, SqlConStr);
    SnmpConnector instance = Factory. BuildSnmpConnector(Switches);
    

    我在如何为GetHashCode实现实现EqualsISnmpDataSource方面遇到了一些问题,但正式化相等的定义几乎解决了所有这些问题。

    我对最终结果非常满意; Factory类负责限制实例化,而SnmpConnector负责缓存查询结果,而ISnmpDataSource负责实际运行查询。我确信那里有一个更好的组织,但这个组织足够干净,可以使用。