Кто несет ответственность за кеширование / запоминание результатов функции? - PullRequest
7 голосов
/ 30 ноября 2011

Я работаю над программным обеспечением, которое позволяет пользователю расширять систему путем реализации набора интерфейсов.

Чтобы проверить жизнеспособность того, что мы делаем, моя компания " ест свою собачью еду ", реализовав всю нашу бизнес-логику в этих классах точно так же, как это сделал бы пользователь.

У нас есть несколько служебных классов / методов, которые связывают все вместе и используют логику, определенную в расширяемых классах.


Я хочу кешировать результаты пользовательских функций. Где я должен это сделать?

  • Это сами классы? Кажется, что это может привести к значительному дублированию кода.

  • Это утилиты / движок, которые используют эти классы? В этом случае неинформированный пользователь может напрямую вызывать функцию класса и не получать никаких преимуществ от кэширования.


Пример кода

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" };
    }
}

Я сталкиваюсь с необходимостью сделать наше программное обеспечение максимально быстрым и поддерживать меньше кода (в этом примере: кэширование результата в LetterUtility) и выполнять одну и ту же работу снова и снова (в этом примере: каждые 10 секунд ожидания) время C используется).

Ответы [ 5 ]

10 голосов
/ 30 ноября 2011

Какой слой лучше всего отвечает за кэширование результатов этих определяемых пользователем функций?

Ответ довольно очевиден: слой, который может правильно реализовать требуемую политику кэширования - это правильный уровень.

Правильная политика кэширования должна иметь две характеристики:

  • Она никогда не должна обслуживать устаревшие данные;он должен знать, даст ли кэшируемый метод другой результат, и сделать кеш недействительным в какой-то момент до того, как вызывающий получит устаревшие данныеимя пользователяКэш без политики истечения срока действия, который растет без границ, имеет другое имя: мы обычно называем их «утечками памяти».тайник несвежий?и "кеш слишком большой?" Это слой, который должен реализовывать кеш.

4 голосов
/ 30 ноября 2011

Что-то вроде кеширования можно считать «сквозной» проблемой (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 в приведенном выше примере), который делегирует ваше поведение.

0 голосов
/ 30 ноября 2011

Все предыдущие посты подняли некоторые хорошие моменты, вот очень грубый план того, как вы могли бы это сделать.Я написал это на лету, так что, возможно, потребуется некоторая настройка:

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; 
         }
      }); 
   }   
}
0 голосов
/ 30 ноября 2011

В общем случае кэширование и запоминание имеют смысл, когда:

  1. Получение результата (или, по крайней мере, может быть) с высокой задержкой или иным образом дороже, чем затраты, вызванные самим кэшированием.
  2. Результаты имеют шаблон поиска, в котором будут частые вызовы с одинаковыми входными данными для функции (то есть не только аргументы, но и любой экземпляр, статические и другие данные, влияющие на результат).
  3. В коде, к которому обращается данный код, уже не существует механизма кэширования, который делает это ненужным.
  4. В коде не будет другого механизма кэширования, вызывающего соответствующий код, который делаетв этом нет необходимости (почему почти никогда не имеет смысла запоминать GetHashCode() в этом методе, несмотря на то, что люди часто испытывают искушение, когда реализация является относительно дорогой).
  5. Невозможно стать устаревшим, вряд ли станет устаревшим, покакэш загружен, неважно, устарел ли он или нетsy для обнаружения.

В некоторых случаях каждый вариант использования компонента будет соответствовать всем этим.Есть много, где они не будут.Например, если компонент кэширует результаты, но никогда не вызывается дважды с одними и теми же входными данными конкретным клиентским компонентом, то такое кэширование - это просто трата, которая негативно влияет на производительность (может быть незначительной, а может быть серьезной).1017 * Чаще всего клиентскому коду имеет больше смысла выбирать политику кэширования, которая бы ему подходила.Кроме того, часто будет проще настроить конкретное использование в данный момент перед лицом реальных данных, чем в компоненте (поскольку реальные данные, с которыми он столкнется, могут значительно различаться от использования к использованию).

Еще сложнее узнать, какая степень застоя может быть приемлемой.Как правило, компонент должен предполагать, что от него требуется 100% свежесть, в то время как клиентский компонент может знать, что определенная степень устаревания будет хорошей.

С другой стороны, это может быть проще для компонентадля получения информации, полезной для кеша.В этих случаях компоненты могут работать рука об руку, хотя это гораздо сложнее (примером может служить механизм If-Modified-Since, используемый веб-сервисами RESTful, где сервер может указать, что клиент может безопасно использовать информацию, которую он кэшировал).

Кроме того, компонент может иметь настраиваемую политику кэширования.Пул подключений - это своего рода политика кэширования, подумайте, как это настраивается.

Итак, подведем итог:

Компонент, который может определить, какое кэширование возможно и полезно.

Который чаще всего является клиентским кодом.Хотя здесь могут помочь сведения о вероятной задержке и устаревании, задокументированные авторами компонента.

Реже может быть клиентский код с помощью компонента, хотя для этого необходимо предоставить детали кэширования.

И иногда может быть компонентом с политикой кэширования, настраиваемой с помощью вызывающего кода.

Может быть компонентом только в редких случаях, потому что он реже для всех возможных вариантов использования, которые хорошо обслуживаются одним и тем жеполитика кеширования.Одним из важных исключений является случай, когда один и тот же экземпляр этого компонента будет обслуживать несколько клиентов, поскольку тогда факторы, влияющие на вышесказанное, распространяются на этих нескольких клиентов.

0 голосов
/ 30 ноября 2011

Хотя я не знаю C #, это похоже на случай использования AOP (Aspect-Oriented Programming).Идея состоит в том, что вы можете «ввести» код, который будет выполняться в определенных точках стека выполнения.

Вы можете добавить код кэширования следующим образом:

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

Затем вы определите, что этокод должен выполняться перед каждым вызовом функций вашего интерфейса (и всех подклассов, реализующих эти функции).

Может ли какой-нибудь эксперт .NET рассказать нам, как вы будете это делать в .NET?

...