Alternativas para Singletons para caching de listas de dados?

5

No meu projeto, tenho uma classe Cache abstrata que me permite preencher uma série de listas que persistem globalmente em todo o meu aplicativo. Esses objetos de cache são thread-safe e podem ser manipulados conforme necessário, e permitem que eu reduza a enorme sobrecarga de consultas de APIs externas de terceiros diretamente. Eu já vi um sério ódio por singletons, então estou um pouco curioso sobre as outras opções que tenho quando este é o meu caso de uso atual.

Já vi injeção de dependência mencionada um pouco, mas não sei se é adequada ou útil nesse cenário.

Aqui está um exemplo da minha classe abstrata Cache :

public abstract class Cache<TU, T>
    where TU : Cache<TU, T>, new()
    where T : class
{
    private static readonly TU Instance = new TU();
    private static volatile State _currentState = State.Empty;
    private static volatile object _stateLock = new object();
    private static volatile object _dataLock = new object();
    private static DateTime _refreshedOn = DateTime.MinValue;
    private static T InMemoryData { get; set; }

    public static T Data
    {
        get
        {
            switch (_currentState)
            {
                case State.OnLine:
                    var timeSpentInCache = (DateTime.UtcNow - _refreshedOn);
                    if (timeSpentInCache > Instance.GetLifetime())
                    {
                        lock (_stateLock)
                        {
                            if (_currentState == State.OnLine) _currentState = State.Expired;
                        }
                    }
                    break;

                case State.Empty:
                    lock (_dataLock)
                    {
                        lock (_stateLock)
                        {
                            if (_currentState == State.Empty)
                            {
                                InMemoryData = Instance.GetData();
                                _refreshedOn = DateTime.UtcNow;
                                _currentState = State.OnLine;
                            }
                        }
                    }
                    break;

                case State.Expired:
                    lock (_stateLock)
                    {
                        if (_currentState == State.Expired)
                        {
                            _currentState = State.Refreshing;
                            Task.Factory.StartNew(Refresh);
                        }
                    }
                    break;
            }

            lock (_dataLock)
            {
                if (InMemoryData != null) return InMemoryData;
            }

            return Data;
        }
    }

    public static T PopulateData()
    {
        return Data;
    }

    protected abstract T GetData();

    protected virtual TimeSpan GetLifetime()
    {
        return TimeSpan.FromMinutes(10);
    }

    private static void Refresh()
    {
        if (_currentState != State.Refreshing) return;
        var dt = Instance.GetData();
        lock (_stateLock)
        {
            lock (_dataLock)
            {
                _refreshedOn = DateTime.UtcNow;
                _currentState = State.OnLine;
                InMemoryData = dt;
            }
        }
    }

    public static void Invalidate()
    {
        lock (_stateLock)
        {
            _refreshedOn = DateTime.MinValue;
            _currentState = State.Expired;
        }
    }

    private enum State
    {
        Empty,
        OnLine,
        Expired,
        Refreshing
    }
}

E um exemplo de sua implementação.

public class SalesForceCache
{
    public class Users : Cache<Users, List<Contact>>
    {
        protected override List<Contact> GetData()
        {
            var sf = new SalesForce();
            var users = sf.GetAllUsers();

            sf.Dispose();

            return users;
        }

        protected override TimeSpan GetLifetime()
        {
            try
            {
                return TimeSpan.FromDays(1);
            }
            catch (StackOverflowException)
            {
                return TimeSpan.Zero;
            }
        }
    }

    public class Accounts : Cache<Accounts, List<Account>>
    {
        protected override List<Account> GetData()
        {
            var sf = new SalesForce();
            var accounts = sf.GetAllAccounts();

            sf.Dispose();

            return accounts;
        }

        protected override TimeSpan GetLifetime()
        {
            try
            {
                return TimeSpan.FromDays(1);
            }
            catch (StackOverflowException)
            {
                return TimeSpan.Zero;
            }
        }
    }
}
    
por JD Davis 10.12.2015 / 23:19
fonte

3 respostas

1
  1. Você pode criar uma instância de cache por instância de API de terceiros. Se suas APIs de terceiros não vierem em instâncias, encapsule-as em algum wrapper C # instável.

  2. Você pode tornar seus caches membros do seu único grande objeto Application raiz. Se você não tiver um, crie um. Esta é a única coisa que legitimamente pode ser um singleton. (Mas, mesmo assim, deve ser apenas um singleton no sentido de que será new ed uma vez, não no sentido de consistir em métodos estáticos.)

por 10.12.2015 / 23:30
fonte
0

Não deixe o "ódio sério" por singletons incomodar você. Qualquer um que diga "sempre" ou "nunca" é ignorante. Ou enlouquecido. Ou secretamente chorando por ajuda.

Conceitualmente, um cache é um singleton. Não há como fugir disso. Está sentado na memória em algum lugar, esperando. Muitos leitores e escritores estão acessando aquele pedaço de memória (exceto um cache em disco). Eu imagino que as soluções de injeção de dependência estão falando sobre injetar uma abstração sobre o cache. Pode haver muitos deles, mas se você cavar fundo o suficiente (nem mesmo tão fundo, na verdade), deve haver apenas um cache.

A regra geral da minha equipe é que os singletons podem estar bem para uma única fonte de verdade. Isso soa como o que você está lidando.

Uma observação sobre sua solução atual: No meu mundo ideal, como consumidor casual, eu nunca mais quero ver a palavra "Cache" no código que estou usando. Se eu quiser uma coleção de usuários, prefiro dizer

var store = new UserStore();
return store.GetUsers()

do que

return SalesForceCache.Users.Data;

A palavra "Cache" é sobrecarga mental. Quando eu uso, de repente eu estou pensando sobre o "como" os dados são armazenados versus "o que" eu quero. Claro, pode haver um cache sob o capô do meu UserStore , mas está oculto de mim. E isso é uma coisa boa. Algum outro encanamento deve decidir se algo está em cache e por quanto tempo.

Além disso, há algumas possíveis frustrações futuras:

  1. O mecanismo de armazenamento é rígido. Você só pode armazenar uma coleção. Isso pode não ser o que eu quero. Por exemplo, talvez eu queira armazenar Usuários ativos individualmente, com uma expiração variável, por endereço de e-mail. (Super útil quando você tem muitos usuários, mas apenas um punhado deles está online ao mesmo tempo.)
  2. As expirações são absolutas. Pode ser útil definir uma expiração deslizante. (especialmente sob restrição de memória quando você quer apenas os dados mais ativos na memória)
  3. Não há dependências de cache. As dependências de cache permitem que você expire automaticamente os dados armazenados em cache quando condições específicas são atendidas.

Dê uma olhada no System.Runtime. Caching.MemoryCache para um cache de memória local fácil de usar. É rico em recursos e usa os idiomas comuns em caches chave / valor. Eu absolutamente não quero desencorajá-lo de continuar com sua abordagem! Continue assim! O MemoryCache pode servir como uma referência interessante.

    
por 11.12.2015 / 07:40
fonte
0

... without it having to be passed either as: a) a parameter or b) globally static.

Escolha um. Não há terceira opção.

Você passa a abstração "data query" como parâmetro, com o cache servindo como decorador, ou acessa o cache por meio da propriedade estática globalmente. Eu recomendaria o primeiro, porque ele torna a dependência explícita e permite separar de forma transparente o armazenamento em cache e a aquisição de dados.

Meu comentário sobre o porquê dos Singletons serem ruins, é que eles quebram o SRP. Eles têm duas responsabilidades: o que o singleton deve fazer e entregar sua vida. Esses dois são exclusivos e devem ser separados. O motivo pelo qual as pessoas recomendam o IoC é mover o tempo de vida útil do manuseio para fora do singleton e para o contêiner IoC.

    
por 11.12.2015 / 08:03
fonte