Come evitare di violare l'SRP in una classe per gestire la memorizzazione nella cache?


12

Nota: l'esempio di codice è scritto in c #, ma non dovrebbe importare. Ho messo c # come tag perché non riesco a trovarne uno più appropriato. Riguarda la struttura del codice.

Sto leggendo Clean Code e sto cercando di diventare un programmatore migliore.

Mi trovo spesso a lottare per seguire il principio della responsabilità singola (le classi e le funzioni dovrebbero fare solo una cosa), specialmente nelle funzioni. Forse il mio problema è che "una cosa" non è ben definita, ma comunque ...

Un esempio: ho un elenco di Fluffies in un database. Non ci interessa cosa sia Fluffy. Voglio una classe per recuperare le lanugine. Tuttavia, le lanugine possono cambiare secondo una certa logica. A seconda della logica, questa classe restituirà i dati dalla cache o otterrà le ultime dal database. Potremmo dire che gestisce le lanugine, e questa è una cosa. Per semplificare, supponiamo che i dati caricati siano validi per un'ora e quindi debbano essere ricaricati.

class FluffiesManager
{
    private Fluffies m_Cache;
    private DateTime m_NextReload = DateTime.MinValue;
    // ...
    public Fluffies GetFluffies()
    {
        if (NeedsReload())
            LoadFluffies();

        return m_Cache;
    }

    private NeedsReload()
    {
        return (m_NextReload < DateTime.Now);
    }

    private void LoadFluffies()
    {
        GetFluffiesFromDb();
        UpdateNextLoad();
    }

    private void UpdateNextLoad()
    {
        m_NextReload = DatTime.Now + TimeSpan.FromHours(1);
    }
    // ...
}

GetFluffies()mi sembra ok. L'utente chiede alcuni soffici, li forniamo. Andare a recuperarli dal DB se necessario, ma questo potrebbe essere considerato una parte dell'ottenere le lanugine (ovviamente, è un po 'soggettivo).

NeedsReload()sembra giusto. Verifica se dobbiamo ricaricare le lanugine. UpdateNextLoad va bene. Aggiorna il tempo per la prossima ricarica. questa è sicuramente una cosa sola.

Tuttavia, sento che cosa LoadFluffies()non può essere descritto come una sola cosa. Sta ottenendo i dati dal database e sta pianificando il prossimo ricaricamento. È difficile sostenere che il calcolo del tempo per la prossima ricarica faccia parte del recupero dei dati. Tuttavia, non riesco a trovare un modo migliore per farlo (rinominare la funzione LoadFluffiesAndScheduleNextLoadpotrebbe essere migliore, ma rende il problema più ovvio).

Esiste una soluzione elegante per scrivere davvero questa classe secondo l'SRP? Sono troppo pedante?

O forse la mia classe non sta davvero facendo solo una cosa?


3
Basato su "scritto in C #, ma non dovrebbe importare", "Riguarda la struttura del codice", "Un esempio: ... Non ci interessa cosa sia un Fluffy", "Per semplificare, diciamo ...", questa non è una richiesta di revisione del codice, ma una domanda su un principio di programmazione generale.
200_successo

@ 200_success grazie, e scusa, ho pensato che sarebbe stato adeguato per CR
corvo


2
In futuro starai meglio con "widget" anziché soffice per future domande simili, poiché un widget è inteso come un supporto non particolare per esempi.
whatsisname

1
So che è solo un codice di esempio, ma lo uso in DateTime.UtcNowmodo da evitare i cambi di ora legale o persino un cambiamento nel fuso orario corrente.
Mark Hurd,

Risposte:


23

Se questa classe fosse davvero banale come sembra, non sarebbe necessario preoccuparsi di violare l'SRP. Quindi cosa succede se una funzione a 3 linee ha 2 linee che fanno una cosa e un'altra 1 linea che fa un'altra cosa? Sì, questa banale funzione viola l'SRP, e allora? Che importa? La violazione dell'SRP inizia a diventare un problema quando le cose si complicano.

Il tuo problema in questo caso particolare deriva molto probabilmente dal fatto che la classe è più complicata delle poche righe che ci hai mostrato.

In particolare, il problema molto probabilmente risiede nel fatto che questa classe non solo gestisce la cache, ma probabilmente contiene anche l'implementazione del GetFluffiesFromDb()metodo. Quindi, la violazione dell'SRP è nella classe, non nei pochi metodi banali mostrati nel codice che hai pubblicato.

Quindi, ecco un suggerimento su come gestire tutti i tipi di casi che rientrano in questa categoria generale, con l'aiuto del modello Decoratore .

/// Provides Fluffies.
interface FluffiesProvider
{
    Fluffies GetFluffies();
}

/// Implements FluffiesProvider using a database.
class DatabaseFluffiesProvider : FluffiesProvider
{
    public override Fluffies GetFluffies()
    {
        ... load fluffies from DB ...
        (the entire implementation of "GetFluffiesFromDb()" goes here.)
    }
}

/// Decorates FluffiesProvider to add caching.
class CachingFluffiesProvider : FluffiesProvider
{
    private FluffiesProvider decoree;
    private DateTime m_NextReload = DateTime.MinValue;
    private Fluffies m_Cache;

    public CachingFluffiesProvider( FluffiesProvider decoree )
    {
        Assert( decoree != null );
        this.decoree = decoree;
    }

    public override Fluffies GetFluffies()
    {
        if( DateTime.Now >= m_NextReload ) 
        {
             m_Cache = decoree.GetFluffies();
             m_NextReload = DatTime.Now + TimeSpan.FromHours(1);
        }
        return m_Cache;
    }
}

ed è usato come segue:

FluffiesProvider provider = new DatabaseFluffiesProvider();
provider = new CachingFluffiesProvider( provider );
...go ahead and use provider...

Nota come CachingFluffiesProvider.GetFluffies()non ha paura di contenere il codice che fa il controllo e l'aggiornamento del tempo, perché è roba banale. Ciò che fa questo meccanismo è indirizzare e gestire l'SRP a livello di progettazione del sistema, dove è importante, non a livello di piccoli metodi individuali, dove comunque non importa.


1
+1 per riconoscere che lanugine, memorizzazione nella cache e accesso alla banca dati sono in realtà tre responsabilità. Potresti anche provare a rendere generica l'interfaccia di FluffiesProvider e i decoratori (IProvider <Fluffy>, ...) ma questo potrebbe essere YAGNI.
Roman Reiner,

Onestamente, se esiste un solo tipo di cache e estrae sempre oggetti dal database, questo è IMHO fortemente sovrascritto (anche se la classe "reale" potrebbe essere più complessa come possiamo vedere nell'esempio). L'astrazione solo per motivi di astrazione non rende il codice più pulito o più mantenibile.
Doc Brown,

Il problema di @DocBrown è la mancanza di contesto alla domanda. Mi piace questa risposta perché mostra un modo in cui ho usato più e più volte in applicazioni più grandi e poiché è facile scrivere test contro, mi piace anche la mia risposta perché è solo una piccola modifica e produce qualcosa di chiaro senza alcuna sovrascrittura in modo che attualmente si trova, senza contesto praticamente tutte le risposte qui sono buone:]
stijn

1
FWIW, la classe che avevo in mente quando ho posto la domanda è più complicata di FluffiesManager, ma non eccessivamente. Circa 200 righe, forse. Non ho posto questa domanda perché non ho riscontrato alcun problema con il mio progetto (ancora?), Solo perché non sono riuscito a trovare un modo per rispettare rigorosamente l'SRP e ciò potrebbe costituire un problema in casi più complessi. Quindi, la mancanza di contesto è in qualche modo intesa. Penso che questa risposta sia fantastica.
corvo,

2
@stijn: beh, penso che la tua risposta sia fortemente sottovalutata. Invece di aggiungere inutili astrazioni, basta tagliare e nominare le responsabilità in modo diverso, che dovrebbe essere sempre la prima scelta prima di accumulare tre strati di eredità per un problema così semplice.
Doc Brown,

6

La tua stessa classe mi sembra a posto, ma hai ragione LoadFluffies(), non esattamente quello che pubblicizza il nome. Una soluzione semplice sarebbe quella di cambiare il nome e spostare il ricaricamento esplicito da GetFluffies, in una funzione con una descrizione appropriata. Qualcosa di simile a

public Fluffies GetFluffies()
{
  MakeSureTheFluffyCacheIsUpToDate();
  return m_Cache;
}

private void MakeSureTheFluffyCacheIsUpToDate()
{
  if( !NeedsReload )
    return;
  GetFluffiesFromDb();
  SetNextReloadTime();
}

mi sembra pulito (anche perché come dice Patrick: è composto da altre minuscole funzioni obbedienti alla SRP), e soprattutto anche chiaro che a volte è altrettanto importante.


1
Mi piace la semplicità in questo.
corvo,

6

Credo che la tua classe stia facendo una cosa; è una cache di dati con un timeout. LoadFluffies sembra un'astrazione inutile a meno che non la chiami da più punti. Penso che sarebbe meglio prendere le due righe da LoadFluffies e metterle nel condizionale NeedsReload in GetFluffies. Ciò renderebbe molto più ovvia l'implementazione di GetFluffies ed è ancora un codice pulito, poiché stai componendo subroutine a responsabilità singola per raggiungere un unico obiettivo, un recupero cache dei dati dal db. Di seguito è riportato il metodo get fluffies aggiornato.

public Fluffies GetFluffies()
{
    if (NeedsReload()) {
        GetFluffiesFromDb();
        UpdateNextLoad();
    }

    return m_Cache;
}

Anche se questa è una buona prima risposta, tieni presente che il codice "risultato" è spesso una buona aggiunta.
Finanzi la causa di Monica il

4

Il tuo istinto è corretto. La tua classe, per quanto piccola, sta facendo troppo. È necessario separare la logica della cache di aggiornamento temporizzato in una classe completamente generica. Quindi crea un'istanza specifica di quella classe per la gestione di Fluffies, qualcosa del genere (non compilato, il codice di lavoro viene lasciato come esercizio per il lettore):

public class TimedRefreshCache<T> {
    T m_Value;
    DateTime m_NextLoadTime;
    Func<T> m_producer();
    public CacheManager(Func<T> T producer, Interval timeBetweenLoads) {
          m_nextLoadTime = INFINITE_PAST;
          m_producer = producer;
    }
    public T Value {
        get {
            if (m_NextLoadTime < DateTime.Now) {
                m_Value = m_Producer();
                m_NextLoadTime = ...;
            }
            return m_Value;
        }
    }
}

public class FluffyCache {
    private TimedRefreshCache m_Cache 
        = new TimedRefreshCache<Fluffy>(GetFluffiesFromDb, interval);
    private Fluffy GetFluffiesFromDb() { ... }
    public Fluffy Value { get { return m_Cache.Value; } }
}

Un ulteriore vantaggio è che ora è molto facile testare TimedRefreshCache.


1
Concordo sul fatto che se la logica di aggiornamento diventa più complicata rispetto all'esempio, potrebbe essere una buona idea trasformarla in una classe separata. Ma non sono d'accordo sul fatto che la classe nell'esempio faccia troppo.
Doc Brown,

@kevin, non ho esperienza con TDD. Potresti approfondire come testeresti TimedRefreshCache? Non lo vedo come "molto semplice", ma potrebbe essere la mia mancanza di competenza.
corvo,

1
Personalmente non mi piace la tua risposta a causa della sua complessità. È molto generico e molto astratto e può essere il migliore in situazioni più complicate. Ma in questo semplice caso è "semplicemente troppo". Dai un'occhiata alla risposta di stijn. Che risposta simpatica, breve e leggibile. Tutti lo capiranno immediatamente. Cosa pensi?
Dieter Meemken,

1
@raven Puoi testare TimedRefreshCache usando un breve intervallo (come 100ms) e un produttore molto semplice (come DateTime.Now). Ogni 100 ms la cache produrrà un nuovo valore, in mezzo restituirà il valore precedente.
Kevin Cline,

1
@DocBrown: il problema è che come scritto non è testabile. La logica di temporizzazione (testabile) è accoppiata con la logica del database, che viene poi derisa. Una volta creata una cucitura per deridere la chiamata al database, si è al 95% della soluzione generica. Ho scoperto che costruire queste piccole classi di solito paga perché finiscono per essere riutilizzate più del previsto.
Kevin Cline,

1

La tua classe va bene, SRP riguarda una classe non una funzione, l'intera classe è responsabile di fornire i "Fluffies" dalla "Fonte dei dati", quindi sei libero nell'implementazione interna.

Se si desidera espandere il meccanismo di cahing, è possibile creare una classe responsabile per la visione dell'origine dati

public class ModelWatcher
{

    private static Dictionary<Type, DateTime> LastUpdate;

    public static bool IsUpToDate(Type entityType, DateTime lastRead) {
        if (LastUpdate.ContainsKey(entityType)) {
            return lastRead >= LastUpdate[entityType];
        }
        return true;
    }

    //call this method whenever insert/update changed to any entity
    private void OnDataSourceChanged(Type changedEntityType) {
        //update Date & Time
        LastUpdate[changedEntityType] = DateTime.Now;
    }
}
public class FluffyManager
{
    private DateTime LastRead = DateTime.MinValue;

    private List<Fluffy> list;



    public List<Fluffy> GetFluffies() {

        //if first read or not uptodated
        if (list==null || !ModelWatcher.IsUpToDate(typeof(Fluffy),LastRead)) {
            list = ReadFluffies();
        }
        return list;
    }
    private List<Fluffy> ReadFluffies() { 
    //read code
    }
}

Secondo lo zio Bob: LE FUNZIONI DOVREBBE FARE UNA COSA. DOVREBBERO FARLO BENE. DOVREBBERO FARLO SOLO. Codice pulito p.35.
corvo,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.