8

我正在开发允许用户通过实现一组接口来扩展系统的软件。

为了测试我们所做工作的可行性,我的公司通过以与用户完全相同的方式在这些类中实现我们所有的业务逻辑来“吃自己的狗粮”。

我们有一些实用程序类/方法将所有内容联系在一起并使用可扩展类中定义的逻辑。


我想缓存用户定义函数的结果。我应该在哪里做这个?

  • 是班级本身吗?这似乎会导致大量代码重复。

  • 是使用这些类的实用程序/引擎吗?如果是这样,不知情的用户可能会直接调用类函数而不会获得任何缓存好处。


示例代码

public interface ILetter { string[] GetAnimalsThatStartWithMe(); }

public class A : ILetter { public string[] GetAnimalsThatStartWithMe()
                           { 
                               return new [] { "Aardvark", "Ant" }; 
                           }
                         }
public class B : ILetter { public string[] GetAnimalsThatStartWithMe()
                           { 
                               return new [] { "Baboon", "Banshee" }; 
                           } 
                         }
/* ...Left to user to define... */
public class Z : ILetter { public string[] GetAnimalsThatStartWithMe()
                           { 
                               return new [] { "Zebra" };
                           }
                         }

public static class LetterUtility
{
    public static string[] GetAnimalsThatStartWithLetter(char letter)
    {
        if(letter == 'A') return (new A()).GetAnimalsThatStartWithMe();
        if(letter == 'B') return (new B()).GetAnimalsThatStartWithMe();
        /* ... */
        if(letter == 'Z') return (new Z()).GetAnimalsThatStartWithMe();
        throw new ApplicationException("Letter " + letter + " not found");
    }
}

LetterUtility 是否应该负责缓存?每个单独的ILetter实例都应该吗?还有其他完全可以做的事情吗?

我试图使这个示例简短,因此这些示例函数不需要缓存。但是考虑一下我添加了这个类,(new C()).GetAnimalsThatStartWithMe()它每次运行都需要 10 秒:

public class C : ILetter
{
    public string[] GetAnimalsThatStartWithMe()
    {
        Thread.Sleep(10000);
        return new [] { "Cat", "Capybara", "Clam" };
    }
}

我发现自己在使我们的软件尽可能快和维护更少的代码(在本例中:将结果缓存LetterUtilityC

4

5 回答 5

11

哪一层最负责缓存这些用户可定义函数的结果?

答案很明显:能够正确实现所需缓存策略的层就是正确的层。

一个正确的缓存策略需要有两个特征:

  • 它绝不能提供过时的数据;它必须知道被缓存的方法是否会产生不同的结果,并在调用者获得陈旧数据之前的某个时间点使缓存无效

  • 它必须代表用户有效地管理缓存资源。没有过期策略且无限增长的缓存有另一个名称:我们通常称它们为“内存泄漏”。

系统中的哪个层知道“缓存是否陈旧”问题的答案?和“缓存太大了吗?” 那是应该实现缓存的层。

于 2011-11-30T16:15:20.903 回答
4

像缓存这样的东西可以被认为是一个“交叉”问题(http://en.wikipedia.org/wiki/Cross-cutting_concern):

在计算机科学中,横切关注点是影响其他关注点的程序方面。在设计和实现中,这些问题通常无法从系统的其余部分中彻底分解,并且可能导致分散(代码重复)、缠结(系统之间的重要依赖关系)或两者兼而有之。例如,如果编写一个用于处理医疗记录的应用程序,那么这些记录的簿记和索引是核心问题,而将更改历史记录到记录数据库或用户数据库或身份验证系统将是横切关注点,因为他们触及程序的更多部分。

横切关注点通常可以通过面向方面的编程(http://en.wikipedia.org/wiki/Aspect-oriented_programming)来实现。

在计算中,面向方面编程 (AOP) 是一种编程范式,旨在通过允许分离横切关注点来增加模块化。AOP 构成了面向方面的软件开发的基础。

.NET 中有许多工具可以促进面向方面的编程。我最喜欢那些提供完全透明实现的。在缓存示例中:

public class Foo
{
    [Cache(10)] // cache for 10 minutes
    public virtual void Bar() { ... }
}

这就是您需要做的一切......通过定义如下行为自动发生其他一切:

public class CachingBehavior
{
   public void Intercept(IInvocation invocation) { ... } 
   // this method intercepts any method invocations on methods attributed with the [Cache] attribute. 
  // In the case of caching, this method would check if some cache store contains the data, and if it does return it...else perform the normal method operation and store the result
}

有两个一般学校是如何发生这种情况的:

  1. 后期构建 IL 编织。PostSharp、Microsoft CCI 和 Mono Cecil 等工具可以配置为自动重写这些属性化方法,以自动委托给您的行为。

  2. 运行时代理。Castle DynamicProxy 和 Microsoft Unity 之类的工具可以自动生成代理类型(从 Foo 派生的类型,在上面的示例中覆盖 Bar),委托给您的行为。

于 2011-11-30T16:39:38.533 回答
0

一般来说,缓存和记忆在以下情况下是有意义的:

  1. 获得结果是(或至少可能是)高延迟或比缓存本身造成的费用昂贵。
  2. 结果具有查找模式,其中将频繁调用具有相同输入的函数(即,不仅是参数,还有影响结果的任何实例、静态和其他数据)。
  3. 在有问题的代码调用的代码中没有一个已经存在的缓存机制,这使得这变得不必要。
  4. 代码中不会有另一种缓存机制调用有问题的代码,这使得这变得不必要(为什么GetHashCode()在该方法中记忆几乎没有意义,尽管人们经常在实现相对昂贵时受到诱惑)。
  5. 不可能变得陈旧,在缓存加载时不太可能变得陈旧,如果它变得陈旧则不重要,或者陈旧很容易检测到。

在某些情况下,组件的每个用例都将匹配所有这些。还有更多他们不会的地方。例如,如果一个组件缓存了结果,但从未被特定客户端组件以相同的输入调用两次,那么缓存只是一种浪费,对性能产生了负面影响(可能可以忽略不计,也可能很严重)。

更常见的是,客户端代码决定适合它的缓存策略更有意义。面对现实世界的数据,此时针对特定用途进行调整通常也比在组件中更容易(因为它所面临的现实世界数据可能因用途而异)。

更难知道什么程度的陈旧是可以接受的。通常,一个组件必须假设它需要 100% 的新鲜度,而客户端组件可以知道一定程度的陈旧性就可以了。

另一方面,组件更容易获得对缓存有用的信息。在这些情况下,组件可以协同工作,尽管涉及更多(例如 RESTful Web 服务使用的 If-Modified-Since 机制,其中服务器可以指示客户端可以安全地使用它已缓存的信息)。

此外,组件可以具有可配置的缓存策略。连接池是一种缓存策略,考虑一下它是如何配置的。

总而言之:

可以计算出哪些缓存是可能的和有用的组件。

这通常是客户端代码。尽管组件作者记录的可能延迟和陈旧的详细信息在这里会有所帮助。

在组件的帮助下,客户端代码可以更少,尽管您必须公开缓存的详细信息以允许这样做。

并且有时可以是调用代码可配置缓存策略的组件。

很少能成为组件,因为所有可能的用例都很少被相同的缓存策略很好地服务。一个重要的例外是该组件的同一个实例将服务于多个客户端,因为影响上述情况的因素分布在这些多个客户端上。

于 2011-11-30T16:22:55.347 回答
0

虽然我不懂 C#,但这似乎是使用 AOP(面向方面​​编程)的一个案例。这个想法是您可以“注入”要在执行堆栈中的某些点执行的代码。

您可以按如下方式添加缓存代码:

IF( InCache( object, method, method_arguments ) )
  RETURN Cache(object, method, method_arguments);
ELSE
  ExecuteMethod(); StoreResultsInCache();

然后,您定义应该在每次调用接口函数(以及实现这些函数的所有子类)之前执行此代码。

一些 .NET 专家能否启发我们如何在 .NET 中执行此操作?

于 2011-11-30T16:14:11.173 回答
0

之前的所有帖子都提出了一些好的观点,这里是一个非常粗略的概述,您可以这样做。我是即时写的,所以可能需要一些调整:

interface IMemoizer<T, R>
{
   bool IsValid(T args); //Is the cache valid, or stale, etc. 
   bool TryLookup(T args, out R result);    
   void StoreResult(T args, R result); 
}

static IMemoizerExtensions
{
   Func<T, R> Memoizing<T, R>(this IMemoizer src, Func<T, R> method)
   {
      return new Func<T, R>(args =>
      {
         R result;

         if (src.TryLookup(args, result) && src.IsValid(args))
         {
            return result; 
         }
         else
         {
            result = method.Invoke(args); 
            memoizer.StoreResult(args, result); 
            return result; 
         }
      }); 
   }   
}
于 2011-11-30T16:51:25.693 回答