Pull to refresh

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 контейнера первое выглядело бы как
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. Совсем.
Тогда это не снаружи класса, а одна из имплементаций.

Это снаружи. Потому что сам класс — это MyDataProvider, он ничего не знает про кэширование. А для декоратора класса может и не быть, если мы интерцепторы используем.

ISomeDataProvider provider = new CachedDataProvider(new MyDataProvider()) вот это вот не выглядит, как интерфейc

Ну так не надо так делать (я вроде и не предлагал нигде). DI, SL, фабрики. Все потребители зависят от интерфейса и только от него.
ISomeDataProvider provider = new CachedDataProvider(new MyDataProvider())

Я это написал только, чтобы можно было лучше понять суть. Писать в проде так не надо, конечно.
Если выбирать из двух — то второе конечно лучше. Т.к. доступ к кешу реализован в отдельной абстракции.
Но все таки мне кажется не совсем правильно выделать абстрактный класс BaseUseCacheStoredService только ради простоты переиспользования удобных методов работы с кешем. Я бы тут наследование, заменил композицией — методы из этого сервиса перенес в IShorttemStore и дергал бы их напрямую. А IShorttemStore инжектил бы только в тот сервис где оно нужно. Это решает проблему, что если нужно несколько видов кешей в одном сервисе, или разные виды кеша в зависимости от окружения, для одного и того же сервиса. Наследование в данном случае не гибко т.к. предполагает только один вид кеша. С реализациями методов в IShorttemStore можно допустим для консольного приложения поднять один и тот же сервис с реализацией кеша в памяти, а в веб приложении, с реализацией кеша в сессии и за счет полиморфизма реализации сервиса будет все равно с каким кешем она работает.
UFO just landed and posted this here
Мне кажется не очень хорошей идеей выносить перегрузки методов в интерфейс. Чаще всего реальная логика содержится только в одном из методов, а остальные делегируют ему. В итоге приходится копипастить эту тривиальную реализацию в каждый класс, реализующий интерфейс, или городить базовый класс. Лучше оставить только самую общую перегрузку в интерфейсе, а остальные сделать методами расширения. Плюс такого подхода — перегрузки достаточно протестировать один раз, минус — невозможность подсунуть оптимизированную реализацию или залезть во внутренности класса.

А для кэширования в последнем проекте я применил аспектно-ориентированное программирование (для начала в рантайме через 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.