Кэширование для больших и маленьких

Задача и простейшее решение

Первочередная задача любой информационной системы — это получение данных. И с решением этой задачей всё ясно: берём и пишем функцию, которая вычисляет требуемые данные. Блок-схема этого решения до неприличия проста и я не буду её здесь приводить. Задача же кэширования — увеличение скорости работы за счёт сохранения промежуточных или вычисляемых результатов в памяти так, чтобы доступ к ним был быстрее, чем повторное вычисление. Таким образом, помимо вычисления результат, его надо иметь возможность куда-то сохранить и оттуда же достать.

Блок-схема вычисления данных
Таким образом, мы имеем вполне стандартный и очевидный алгоритм кэширования данных:

  1. Получаем закэшированные данные из хранилища;
  2. Проверить валидность полученных данных;
  3. Если данные из кэша невалидны:
    1. Вычислить результат на основе имеющихся данных
    2. Сохранить результат в хранилище
  4. Вернуть результат и выйти из функции

Тут всё ясно и объектная модель кажется простой и понятной:


Что-то здесь не так! (Рефакторинг)

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

Действительно, если мы посмотрим на объектную модель, то увидим следующие классы и роли:

  1. CacheStorage — предоставляет интерфейс для доступа к некоему хранилищу;
  2. DataProducer — по логике, должен предоставлять интерфейс для вычисления данных, но на практике он также содержит в себе реализацию логику кэширования, что явно противоречит принципу единственной обязанности;

Таким образом, чтобы явно указать, что для некоторых данных мы намерены использовать кэширование, нам необходимо объявить отдельный класс. Хочется отметить, что эта задача является классической для применения шаблона проектирования Proxy. В итоге, мы получаем следующую модель и реализацию:

От наследования к агрегации и функциональному программированию

Казалось бы мы справились с задачей: у нас есть замечательны код, который позволяет использовать (или не использовать, если надо) кэширование данных, с чётко определёнными обязанностями у классов. Но «проблемы» возникают, если классов и методов, результаты выполнения которых мы хотим закэшировать, становится всё больше. В таком случае, необходимо для каждого из такого класса необходимо создавать свой прокси-класс, в котором необходимо для каждой функции писать более или менее одинаковую обёртку, которая бы валидировала данные в кэше. Хотелось бы сделать так, чтобы функция кэширования совсем не зависела от структуры нашей программы. на этом этапе я перешёл на тёмную сторону силы к функциональному подходу: мы можем явно объявить результаты каких функций мы хотим закэшировать с помощью делегатов и потом всегда явно обращаться к объекту кэша, чтобы получить эти данные.


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

Заключение

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

Поделиться
This entry was posted in Programming and tagged , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>