Comments 24
Осталось к SOLID добавить DI, и получите свой идеал архитектуры, наверное. И ещё кэш реализовать не в виде сервиса, а в виде аспекта протокола (как в REST/HTTP, например). Потом выложить на TodoMVC, и в мире родится очередная серебряная пуля для архитектуры.
мне не нравится класс SessionDataServiceBase, а именно метод T Get(string key, Func getData), который не только работает с сессией, но и занимается обработкой данных.
Зря вы так. Какая там обработка данных? Вот код из одного моего проекта
public class InMemoryCache : ICache
{
public const int CacheMinutes = 10;
public T GetOrSet<T>(string cacheKey, Func<T> getItemCallback) where T : class
{
T item = MemoryCache.Default.Get(cacheKey) as T;
if (item == null)
{
item = getItemCallback();
MemoryCache.Default.Add(cacheKey, item, DateTime.Now.AddMinutes(CacheMinutes));
}
return item;
}
Вся логика обработки данных остаётся в соответствующих сервисах, а в этом тупо получение/сохранение данных в кэше. Это очень удобно.
(например, консольное приложение не имеет сессии вообще), поэтому работу с ними реализуют тоже сервисы
А зачем вообще использовать сессии, особенно на уровне бизнес логики? Если хочется закэшировать данные только для конкретного пользователя, используйте составной ключ cacheKey = cacheKeyPrefix + separator + user.Id.
Я ещё могу понять редкие случаи использования сессий на уровне web-приложения, но тащить их ниже на уровень бизнесс-логики — это ошибка протекающих абстракций.
Есть данные конкретного пользователя. которые генерируются из каких то общих данных и данных конкретного пользователя. Обработка этих данных бывает весьма витиевата и ей самое место в бизнес логике. Каждых раз доставать общие данные из базы, даже если учитывать, что сам сиквел их кеширует, не есть хорошо, так как это самое узкое место в проекте.
А по поводу генерации ключа — он и генерируется по подобным правилам. Это можно сделать как базовый класс и все будет генерироваться автоматически. А можно вручную и тогда за этим нужно следить, чтобы все придерживались определенных правил и не косячили. А человеческий фактор не косячить к сожелению не может. Вот во втором случае и есть попытка уйти от этого фактора
А по поводу генерации ключа — он и генерируется по подобным правилам. Это можно сделать как базовый класс и все будет генерироваться автоматически. А можно вручную и тогда за этим нужно следить, чтобы все придерживались определенных правил и не косячили. А человеческий фактор не косячить к сожелению не может. Вот во втором случае и есть попытка уйти от этого фактора
По-хорошему, кэширование, как и прочие cross-cutting concerns, должно жить «снаружи» класса, который поставляет данные. Проще говоря, есть у нас
(особо хитрые/ленивые люди делают автоматическую генерацию таких оберток, но тут, как всегда, есть нюансы)
ISomeDataProvider
, от которого зависит бизнес-код. В какой-то момент мы поняли, что этот провайдер — медленный, и надо бы сделать кэш — создаем новый класс, реализующий кэширование, заставляем его реализовать тот же ISomeDataProvider
, а за данными из него ходим в «старую» реализацию. На уровне composition root заменяем старую реализацию на новую — и все.(особо хитрые/ленивые люди делают автоматическую генерацию таких оберток, но тут, как всегда, есть нюансы)
Это называется декоратор :)
А там реализованна паттерн стратегия. Который тоже неплохо справился с поставленой задачей.
Мне кажется, это самое удачное решение.
У вас есть публичное апи, которое предоставляет ISomeDataProvider. А теперь расскажите, пожалуйста, как вы будете кэширование делать и как замените старую реализацию новой? Даже в композишн рут. А если в композишн руте он юзается много где. Вы его везде будете менять? ИДЕ и идеевский рефакторинг в расчет не берем. Если мы заговорили уже о композинш руте и ДИ, то датапровайдер должен быть интерефейсом, а реализация инжекстится. И если у нас зависит бизнес код от провайдера, то тут композишн рутом не отделаешься и придется менять еще и бизнес код и лезть в чужие модули, которые делает другая команда. Либо делаете интерфейс и реализуете сколько хотите имплементаций с различными кэшами и без, а если по религиозным причинам этого сделать нельзя, то остается делать кэш только внутри класса и никак иначе.
Зачем что то менять?
Как я понял, значально есть реализация ISomeDataProvider (например, MyDataProvider). Далее, делаем вторую реализацию CachedDataProvider. Без IoC контейнера первое выглядело бы как
подмена с кешированием станет выглядеть как
К примеру, такое декорирование Castle Windsor поддерживает из коробки, достаточно указать, что для резолва ISomeDataProvider нужен не только MyDataProvider, но и CachedDataProvider.
Как я понял, значально есть реализация ISomeDataProvider (например, MyDataProvider). Далее, делаем вторую реализацию CachedDataProvider. Без IoC контейнера первое выглядело бы как
ISomeDataProvider provider = new MyDataProvider();
подмена с кешированием станет выглядеть как
ISomeDataProvider provider = new CachedDataProvider(new MyDataProvider());
К примеру, такое декорирование Castle Windsor поддерживает из коробки, достаточно указать, что для резолва ISomeDataProvider нужен не только MyDataProvider, но и CachedDataProvider.
как вы будете кэширование делать?
tym32167 все более-менее правильно описал. Предположим, у нас есть
MyDataProvider: ISomeDataProvider
. А еще, поскольку мы уже знаем, что будем работать в разных средах, у нас есть ICache
(WebCache: ICache
и ConsoleCache: ICache
).Осталось это только собрать:
class MyCachedDataProvider: ISomeDataProvider
{
public MyCachedDataProvider(ISomeDataProvider provider, ICache cache)
{
//..
}
}
Дальше те методы
ISomeDataProvider
, которые нуждаются в кэшировании, оборачиваются (внутри MyCachedDataProvider
) в кэширующую логику, все остальные проксируются как есть.как замените старую реализацию новой
Если у меня dependency injection или service locator, то в момент регистрации сервиса для
ISomeDataProvider
я укажу MyCachedDataProvider
, а дальше все взлетит само (реальное количество конфигурации зависит от используемого контейнера, но суть одна и та же). Если у вас фабрики — придется править фабрики. Если у вас прямое создание — тогда вы в беде, но вы в беде безотносительно кэширования.(повторюсь, если у вас сервисов, нуждающихся в кэшировании, много, а логика кэширования везде одинаковая, то можно перестать писать декораторы вручную и взять контейнер, позволяющий генерить интерцепторы на лету)
Ну по сути это можно же сделать и интерфейсом. Почему вы выбрали именно наследование от класса, а не имплементацию интерфейса? В датапровайдере я так понимаю все-равно кэширование будет происходить в методе гетДата. И кэш будет юзаться именно там. Допустим у нас есть инмемори кэш и кэш из базы (в случае сложных систем, которые лезут по всу в другие за данными. опускаем проверку актуальности кэша в этом случае для примера). Как вы совместите коммон метод гетДата из суперкласса с предковым? Или вы оверрайдить будете гетдату? Если оверрайдить, то в любом случае по-моему лучше интерфейс сделать. Налседуются от классов обычно, когда есть общие методы для предков, которые не меняются либо для расширения функционала для консьюмеров.
Почему вы выбрали именно наследование от класса, а не имплементацию интерфейса?
Простите, а где вы видите наследование от класса? У меня везде именно реализация интерфейсов.
(остальное пока обсуждать не вижу смысла, есть подозрение, что вы не поняли схему)
Тогда это не снаружи класса, а одна из имплементаций.
ISomeDataProvider provider = new CachedDataProvider(new MyDataProvider()) вот это вот не выглядит, как интерфейc. Совсем.
ISomeDataProvider provider = new CachedDataProvider(new MyDataProvider()) вот это вот не выглядит, как интерфейc. Совсем.
Тогда это не снаружи класса, а одна из имплементаций.
Это снаружи. Потому что сам класс — это
MyDataProvider
, он ничего не знает про кэширование. А для декоратора класса может и не быть, если мы интерцепторы используем.ISomeDataProvider provider = new CachedDataProvider(new MyDataProvider()) вот это вот не выглядит, как интерфейc
Ну так не надо так делать (я вроде и не предлагал нигде). DI, SL, фабрики. Все потребители зависят от интерфейса и только от него.
Если выбирать из двух — то второе конечно лучше. Т.к. доступ к кешу реализован в отдельной абстракции.
Но все таки мне кажется не совсем правильно выделать абстрактный класс BaseUseCacheStoredService только ради простоты переиспользования удобных методов работы с кешем. Я бы тут наследование, заменил композицией — методы из этого сервиса перенес в IShorttemStore и дергал бы их напрямую. А IShorttemStore инжектил бы только в тот сервис где оно нужно. Это решает проблему, что если нужно несколько видов кешей в одном сервисе, или разные виды кеша в зависимости от окружения, для одного и того же сервиса. Наследование в данном случае не гибко т.к. предполагает только один вид кеша. С реализациями методов в IShorttemStore можно допустим для консольного приложения поднять один и тот же сервис с реализацией кеша в памяти, а в веб приложении, с реализацией кеша в сессии и за счет полиморфизма реализации сервиса будет все равно с каким кешем она работает.
Но все таки мне кажется не совсем правильно выделать абстрактный класс BaseUseCacheStoredService только ради простоты переиспользования удобных методов работы с кешем. Я бы тут наследование, заменил композицией — методы из этого сервиса перенес в IShorttemStore и дергал бы их напрямую. А IShorttemStore инжектил бы только в тот сервис где оно нужно. Это решает проблему, что если нужно несколько видов кешей в одном сервисе, или разные виды кеша в зависимости от окружения, для одного и того же сервиса. Наследование в данном случае не гибко т.к. предполагает только один вид кеша. С реализациями методов в IShorttemStore можно допустим для консольного приложения поднять один и тот же сервис с реализацией кеша в памяти, а в веб приложении, с реализацией кеша в сессии и за счет полиморфизма реализации сервиса будет все равно с каким кешем она работает.
UFO just landed and posted this here
Мне кажется не очень хорошей идеей выносить перегрузки методов в интерфейс. Чаще всего реальная логика содержится только в одном из методов, а остальные делегируют ему. В итоге приходится копипастить эту тривиальную реализацию в каждый класс, реализующий интерфейс, или городить базовый класс. Лучше оставить только самую общую перегрузку в интерфейсе, а остальные сделать методами расширения. Плюс такого подхода — перегрузки достаточно протестировать один раз, минус — невозможность подсунуть оптимизированную реализацию или залезть во внутренности класса.
А для кэширования в последнем проекте я применил аспектно-ориентированное программирование (для начала в рантайме через Castle.DynamicProxy, при необходимости можно генерировать обёртки и на этапе компиляции через Fody или PostSharp). Ключи назначаются автоматически из сигнатуры метода, что избавляет от проблем с забытой частью ключа или коллизиями. Выглядит довольно лаконично:
А для кэширования в последнем проекте я применил аспектно-ориентированное программирование (для начала в рантайме через Castle.DynamicProxy, при необходимости можно генерировать обёртки и на этапе компиляции через Fody или PostSharp). Ключи назначаются автоматически из сигнатуры метода, что избавляет от проблем с забытой частью ключа или коллизиями. Выглядит довольно лаконично:
class CommentService : ICommentService
{
private readonly ICurrentUser _user;
private readonly ICommentRepository _repository;
// ...
[Cache(IsUserScope = true)]
public virtual IReadOnlyList<Comment> GetComments(int postId)
{
var comments = _repository.GetComments(postId);
MarkCurrentUserComments(comments);
return comments;
}
}
Sign up to leave a comment.
Околоархитектурные рассуждения или результаты одного спора