¿Cómo evitar violar el SRP en una clase para administrar el almacenamiento en caché?

12

Nota: El ejemplo de código está escrito en c #, pero eso no debería importar. He puesto c # como etiqueta porque no puedo encontrar una más apropiada. Esto es sobre la estructura del código.

Estoy leyendo Clean Code y trato de convertirme en un mejor programador.

A menudo me encuentro luchando para seguir el Principio de Responsabilidad Única (las clases y funciones deben hacer solo una cosa), especialmente en funciones. Tal vez mi problema es que "una cosa" no está bien definida, pero aún así ...

Un ejemplo: tengo una lista de Fluffies en una base de datos. No nos importa lo que es un mullido. Quiero una clase para recuperar fluffies. Sin embargo, los fluffies pueden cambiar de acuerdo con alguna lógica. Dependiendo de alguna lógica, esta clase devolverá los datos del caché u obtendrá los últimos datos de la base de datos. Podríamos decir que gestiona fluffies, y eso es una cosa. Para hacerlo simple, digamos que los datos cargados son válidos durante una hora y luego se deben volver a cargar.

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() me parece bien. El usuario solicita unos fluffies, nosotros los proporcionamos. Ir a recuperarlos de la base de datos si es necesario, pero eso podría considerarse una parte de obtener los fluffies (por supuesto, eso es algo subjetivo).

NeedsReload() parece correcto, también. Comprueba si necesitamos recargar los fluffies. UpdateNextLoad está bien. Actualiza el tiempo para la próxima recarga. eso es definitivamente una sola cosa.

Sin embargo, siento que LoadFluffies() do no puede ser descrito como una sola cosa. Está obteniendo los datos de la base de datos y está programando la próxima recarga. Es difícil argumentar que calcular el tiempo para la próxima recarga es parte de obtener los datos. Sin embargo, no puedo encontrar una mejor manera de hacerlo (cambiar el nombre de la función a LoadFluffiesAndScheduleNextLoad puede ser mejor, pero solo hace que el problema sea más obvio).

¿Existe una solución elegante para escribir realmente esta clase de acuerdo con el SRP? ¿Estoy siendo demasiado pedante?

¿O quizás mi clase en realidad no está haciendo una sola cosa?

    
pregunta raven 21.11.2015 - 15:39

5 respuestas

23

Si esta clase realmente fuera tan trivial como parece, entonces no habría necesidad de preocuparse por violar el SRP. Entonces, ¿qué sucede si una función de 3 líneas tiene 2 líneas haciendo una cosa y otra línea haciendo otra? Sí, esta función trivial viola el SRP, ¿y qué? ¿A quien le importa? La violación del SRP comienza a convertirse en un problema cuando las cosas se complican.

Su problema en este caso particular probablemente se debe al hecho de que la clase es más complicada que las pocas líneas que nos ha mostrado.

Específicamente, el problema probablemente reside en el hecho de que esta clase no solo administra el caché, sino que también probablemente contiene la implementación del método GetFluffiesFromDb() . Por lo tanto, la violación del SRP está en la clase, no en los pocos métodos triviales que se muestran en el código que usted publicó.

Entonces, aquí hay una sugerencia sobre cómo manejar todo tipo de casos que se encuentran dentro de esta categoría general, con la ayuda de Decorator Patrón .

/// 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;
    }
}

y se utiliza de la siguiente manera:

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

Tenga en cuenta que CachingFluffiesProvider.GetFluffies() no teme contener el código que comprueba y actualiza el tiempo, porque eso es algo trivial. Lo que hace este mecanismo es abordar y manejar el SRP a nivel de diseño del sistema, donde sea importante, no a nivel de pequeños métodos individuales, donde no importa de todos modos.

    
respondido por el Mike Nakis 21.11.2015 - 20:23
6

Tu clase en sí me parece bien, pero tienes razón en que LoadFluffies() no hace exactamente lo que el nombre anuncia. Una solución simple sería cambiar el nombre y mover la recarga explícita de GetFluffies a una función con una descripción adecuada. Algo como

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

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

se ve limpio para mí (también porque, como dice Patrick: está compuesto de otras funciones minúsculas que obedecen a SRP), y sobre todo también está claro, lo que a veces es igual de importante.

    
respondido por el stijn 21.11.2015 - 18:39
6

Creo que tu clase está haciendo una cosa; Es un caché de datos con un tiempo de espera. LoadFluffies parece una abstracción inútil a menos que lo llames desde múltiples lugares. Creo que sería mejor tomar las dos líneas de LoadFluffies y ponerlas en el condicional NeedsReload en GetFluffies. Esto haría que la implementación de GetFluffies sea mucho más obvia y aún sea un código limpio, ya que está componiendo subrutinas de responsabilidad única para lograr un solo objetivo, una recuperación en caché de datos de la base de datos. A continuación se muestra el método actualizado de obtener fluffies.

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

    return m_Cache;
}
    
respondido por el Patrick Goley 21.11.2015 - 15:51
4

Tus instintos son correctos. Tu clase, por pequeña que sea, está haciendo demasiado. Debe separar la lógica de almacenamiento en caché de actualización temporizada en una clase completamente genérica. Luego cree una instancia específica de esa clase para administrar Fluffies, algo como esto (no compilado, el código de trabajo se deja como un ejercicio para el lector):

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; } }
}

Una ventaja adicional es que ahora es muy fácil probar TimedRefreshCache.

    
respondido por el kevin cline 21.11.2015 - 23:02
1

Su clase está bien, SRP se trata de una clase que no es una función, toda la clase es responsable de proporcionar los "Fluffies" desde el "Origen de datos" para que esté libre en la implementación interna.

Si desea expandir el mecanismo de generación de llamadas, puede crear una clase responsable de ver la fuente de datos

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
    }
}
    
respondido por el yousif.aljamri 22.11.2015 - 12:31

Lea otras preguntas en las etiquetas