web-dev-qa-db-ja.com

キャッシュを管理するためにクラスのSRPに違反しないようにする方法は?

注:コードサンプルはc#で記述されていますが、それは問題ではありません。これ以上適切なタグが見つからないため、c#をタグとして使用しました。これはコード構造についてです。

私はClean Codeを読んでいて、より優れたプログラマーになろうとしています。

特に関数の場合、単一の責任の原則(クラスと関数は1つのことだけを実行する必要があります)に従うのに苦労しています。多分私の問題は、「一つのこと」が明確に定義されていないことですが、それでも...

例:データベースにFluffiesのリストがあります。ふわふわが何であるかは気にしません。ふわふわを復習してほしいクラスです。ただし、一部のロジックに従って、ふわふわが変化する場合があります。一部のロジックに応じて、このクラスはキャッシュからデータを返すか、データベースから最新のデータを取得します。ふわふわを管理していると言えるでしょう。簡単にするために、ロードされたデータが1時間有効であり、その後、再ロードする必要があるとします。

_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()は私には問題ないようです。ユーザーはいくつかのフワフワを求め、それらを提供します。必要に応じてそれらをDBから回復しますが、それはフラフを取得することの一部と考えることができます(もちろん、それはやや主観的です)。

NeedsReload()も正しいようです。ふわふわをリロードする必要があるかどうかを確認します。 UpdateNextLoadは問題ありません。次のリロードの時間を更新します。それは間違いなく一つのことです。

しかし、私はLoadFluffies()が1つのこととして説明できないことを感じています。データベースからデータを取得し、次のリロードをスケジュールしています。次のリロードの時間を計算することは、データを取得することの一部であると主張するのは難しいです。しかし、私はそれを行うためのより良い方法を見つけることができません(関数の名前をLoadFluffiesAndScheduleNextLoadに変更する方が良いかもしれませんが、問題がより明白になるだけです)。

SRPに従ってこのクラスを実際に記述するためのエレガントなソリューションはありますか?私は知識が多すぎますか?

または、私のクラスが実際に1つのことだけを行っているのではないでしょうか?

12
raven

このクラスが見た目ほど簡単だった場合、SRPの違反を心配する必要はありません。では、3行の関数が2行で1つの処理を実行し、別の1行で別の処理を実行する場合はどうでしょうか。はい、この些細な機能はSRPに違反しています。誰も気にしない? SRPの違反は、事態が複雑になると問題になり始めます。

この特定のケースでの問題は、おそらく、クラスが私たちが示した数行よりも複雑であるという事実から生じています。

具体的には、問題はおそらくこのクラスがキャッシュを管理するだけでなく、おそらくGetFluffiesFromDb()メソッドの実装も含んでいるという事実にあります。したがって、SRPの違反はクラスにあり、投稿したコードに示されているいくつかの簡単なメソッドではありません。

Decorator Pattern を使用して、この一般的なカテゴリに該当するあらゆる種類のケースを処理する方法についての提案を次に示します。

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

そしてそれは次のように使用されます:

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

CachingFluffiesProvider.GetFluffies()が時間のチェックと更新を行うコードを含むことを恐れないことに注意してください。それは簡単なことだからです。このメカニズムが行うことは、SRPを問題とするシステム設計レベルで処理および処理することであり、とにかく重要ではない小さな個々のメソッドのレベルではありません。

23
Mike Nakis

あなたのクラスは1つのことをやっていると思います。タイムアウトのあるデータキャッシュです。 LoadFluffiesは、複数の場所から呼び出さない限り、役に立たない抽象概念のように見えます。 LoadFluffiesから2行を取得し、GetFluffiesの条件付きのNeedsReloadにそれらを配置する方が良いと思います。これにより、GetFluffiesの実装がより明確になり、単一の目標であるdbからのキャッシュされたデータの取得を実行する単一の責任のサブルーチンを構成するため、クリーンなコードになります。以下は、更新されたget fluffiesメソッドです。

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

    return m_Cache;
}
6
Patrick Goley

あなたのクラス自体は私には問題ないように見えますが、LoadFluffies()は名前が宣伝しているとおりではないということは正しいです。簡単な解決策の1つは、名前を変更して、明示的な再読み込みをGetFluffiesから適切な説明を持つ関数に移動することです。何かのようなもの

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

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

(パトリックが言うように、他の小さなSRP従順な関数で構成されているため)見た目もすっきりしており、特に同じくらい重要な場合もあります。

6
stijn

あなたの本能は正しいです。あなたのクラスは小さいかもしれませんが、あまりにも多くをやっています。時限更新キャッシュロジックを完全に汎用的なクラスに分離する必要があります。次に、次のようなFluffiesを管理するためのそのクラスの特定のインスタンスを作成します(コンパイルされていません。動作するコードは読者の練習問題として残されています)。

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

追加の利点は、TimedRefreshCacheのテストが非常に簡単になることです。

4
kevin cline

あなたのクラスは問題ありません、SRPは関数ではなくクラスに関するものです。クラス全体が「データソース」から「フラッフィー」を提供する責任があるので、内部実装は自由です。

Cahingメカニズムを拡張する場合は、データソースを監視するためのrespnsibleクラスを作成できます。

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
    }
}
1
yousif.aljamri