Alternative a Singletons per la cache di liste di dati?

5

Nel mio progetto, ho una classe astratta Cache che mi consente di popolare una serie di elenchi che persistono globalmente nella mia applicazione. Questi oggetti cache sono sicuri per i thread e possono essere manipolati se necessario, e consentono di ridurre l'enorme sovraccarico di interrogare direttamente le API di terze parti esterne. Ho visto un po 'di odio per i singleton, quindi sono un po' curioso di sapere quali altre opzioni ho quando questo è il mio caso d'uso corrente.

Ho visto un po 'di iniezione di dipendenza, ma non so se è abbastanza adeguato o utile in questo scenario.

Ecco un esempio della mia classe Cache abstract:

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 un esempio della sua implementazione.

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;
            }
        }
    }
}
    
posta JD Davis 10.12.2015 - 23:19
fonte

3 risposte

1
  1. È possibile creare un'istanza cache per l'istanza API di terze parti. Se le API di terze parti non sono presenti in istanze, quindi incapsularle in un wrapper C # istantaneo.

  2. Puoi rendere i membri della cache del tuo unico oggetto% root grandeApplication. Se non ne hai uno, inventane uno. Questa è l'unica cosa che può legittimamente essere un singleton. (Ma anche allora, dovrebbe essere solo un singleton nel senso che sarà new ed una volta, non nel senso di consistere di metodi statici.)

risposta data 10.12.2015 - 23:30
fonte
0

Non lasciare che il "grave odio" per i singleton ti disturbi. Chiunque dice "sempre" o "mai" è ignorante. O squilibrato. O piangendo segretamente per chiedere aiuto.

Concettualmente, una cache è un singleton. Non c'è modo di aggirarlo. È seduto nella memoria da qualche parte, in attesa. Molti lettori e scrittori stanno accedendo a quell'unico blocco di memoria (salvo una cache con supporto del disco). Immagino che le soluzioni di iniezione di dipendenza stiano parlando di iniettare un'astrazione sulla cache. Potrebbero essercene molti, ma se riesci a scavare abbastanza in profondità (neanche così profondo, davvero), dovrebbe esserci sempre una sola cache.

La regola empirica della mia squadra è che i singleton possono essere ok per una singola fonte di verità. Suona come quello che stai affrontando.

Un'osservazione sulla tua attuale soluzione: nel mio mondo ideale, come consumatore occasionale, non voglio mai vedere la parola "Cache" nel codice che sto usando. Se voglio una raccolta di utenti, preferirei dire

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

di

return SalesForceCache.Users.Data;

La parola "cache" è un sovraccarico mentale. Quando lo uso, sto improvvisamente pensando al "come" i dati vengono memorizzati rispetto a "cosa" voglio. Certo, potrebbe esserci una cache sotto il cofano del mio UserStore , ma è nascosta a me. E questa è una buona cosa. Alcuni altri impianti idraulici dovrebbero decidere se qualcosa viene memorizzato nella cache e per quanto tempo.

Inoltre, ci sono alcune possibili frustrazioni future:

  1. Il meccanismo di archiviazione è rigido. Puoi sempre archiviare una collezione. Potrebbe non essere quello che voglio. Ad esempio, forse voglio memorizzare gli utenti attivi singolarmente, con una scadenza scorrevole, per indirizzo email. (Molto utile quando hai molti utenti ma solo una manciata di essi sono online contemporaneamente.)
  2. Le scadenze sono assolute. Può essere utile definire una scadenza di scorrimento. (specialmente quando si ha un vincolo di memoria quando si desidera solo i dati più attivi in memoria)
  3. Non ci sono dipendenze della cache. Le dipendenze della cache consentono di scadere automaticamente i dati memorizzati nella cache quando vengono soddisfatte condizioni specifiche.

Dai un'occhiata a System.Runtime. Caching.MemoryCache per una cache di memoria locale facile da usare. È ricco di funzionalità e utilizza gli idiomi comuni nelle cache chiave / valore. Non voglio assolutamente scoraggiarti dal continuare con il tuo approccio! Continua! Il MemoryCache potrebbe servire come riferimento interessante.

    
risposta data 11.12.2015 - 07:40
fonte
0

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

Scegli uno. Non esiste una terza opzione.

Si passa l'astrazione di "query query" come parametro, con la cache che funge da decoratore o si accede alla cache attraverso la proprietà statica globale. Consiglierei il primo, perché rende esplicita la dipendenza e consente di separare in modo trasparente la memorizzazione nella cache e l'acquisizione dei dati.

La mia opinione sul perché i singleton sono cattivi, è che essi infrangono SRP. Hanno due responsabilità: qualunque cosa si suppone che il singleton faccia e che passi per tutta la vita. Quei due sono esclusivi e dovrebbero essere separati. Motivo per cui le persone consigliano IoC è spostare la gestione a vita al di fuori del singleton e nel contenitore IoC.

    
risposta data 11.12.2015 - 08:03
fonte

Leggi altre domande sui tag