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


12

注:コードサンプルはc#で記述されていますが、それは問題ではありません。適切なものを見つけることができないため、c#をタグとして配置しました。これはコード構造についてです。

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

私はしばしば、単一の責任原則(クラスと関数は1つのことだけを行うべきです)、特に関数に従うことに苦労しています。たぶん私の問題は、「一つのこと」が明確に定義されていないことですが、それでも...

例:データベースにFluffiesのリストがあります。Fluffyが何であるかは気にしません。クラスに綿毛を回復させたい。ただし、綿毛はいくつかのロジックに従って変化する可能性があります。いくつかのロジックに応じて、このクラスはキャッシュからデータを返すか、データベースから最新のデータを取得します。毛羽立ちを管理していると言えますが、それは一つのことです。簡単にするために、ロードされたデータが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()正しいようです。fluffiesをリロードする必要があるかどうかを確認します。UpdateNextLoadは問題ありません。次のリロードの時間を更新します。それは間違いなく一つのことです。

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

SRPに従ってこのクラスを実際に作成するエレガントなソリューションはありますか?私はつまらないですか?

それとも、私のクラスが実際に1つのことだけを行っているわけではありませんか?


3
「C#で記述されていますが、それは重要ではありません」、「これはコード構造に関するものです」、「例:…Fluffyが何であるかは気にしません」、「簡単にするために、…」、これはコードレビューのリクエストではなく、一般的なプログラミングの原則に関する質問です。
200_success

@ 200_successありがとう、申し訳ありませんが、これはCRに十分だと思いました
レイヴン


2
将来的には、将来の同様の質問にふわふわするのではなく、「ウィジェット」を使用するほうがよいでしょう。ウィジェットは、例えば、特定の立場ではないと理解されるからです。
whatsisname

1
私はそれが単なるサンプルコードであることを知ってDateTime.UtcNowいますが、夏時間の切り替えや現在のタイムゾーンの変更さえも避けるために使用します。
マークハード

回答:


23

このクラスが実際に見かけほど些細なものであれば、SRPに違反することを心配する必要はありません。それでは、3行関数に1つのことを行う2行と、別のことを行う別の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を重要なシステム設計レベルで対処し、処理することです。小さな個別メソッドのレベルではなく、とにかく重要ではありません。


1
フラフィ、キャッシング、データベースアクセスは実際には3つの責務であることを認識した+1。FluffiesProviderインターフェースとデコレーターをジェネリック(IProvider <Fluffy>、...)にすることもできますが、これはYAGNIかもしれません。
ローマンライナー

正直に言うと、キャッシュのタイプが1つだけで、常にデータベースからオブジェクトをプルする場合、これは非常に過剰に設計されています(例でわかるように「実際の」クラスがより複雑になる場合もあります)。抽象化のためだけの抽象化は、コードをよりクリーンにしたり、保守しやすくしたりしません。
ドックブラウン

@DocBrownの問題は、質問に対する文脈の欠如です。私はこの答えが好きです。それは、より大きなアプリケーションで何度も何度も使用した方法を示しているためです。テストを書くのは簡単だからです現在のところ、コンテキストなしで、ほとんどすべての回答は良いです:]
stijn

1
FWIW、私が質問したときに私が念頭に置いていたクラスは、FluffiesManagerよりも複雑ですが、過度にそうではありません。たぶん200行ほど。SRPに厳密に準拠する方法を見つけることができなかったという理由だけで、デザインに問題が見つかったので(まだ?)、この質問はしませんでした。したがって、コンテキストの欠如はある程度意図されています。この答えは素晴らしいと思います。
レイヴン

2
@stijn:ええと、あなたの答えは大いに支持されていると思います。不要な抽象化を追加する代わりに、責任を異なる名前に切り分けて名前を付けるだけです。これは、このような単純な問題に3層の継承を重ねる前に常に最初の選択肢となるはずです。
Doc Brown

6

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

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

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

私にとってはきれいに見えます(また、Patrickが言っているように、他の小さなSRP従順な機能で構成されているため)、そして特に重要なこともあります。


1
私はこれのシンプルさが好きです。
レイヴン

6

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

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

    return m_Cache;
}

これはかなり良い最初の答えですが、「結果」コードは多くの場合良い追加物であることに留意してください。
ファンドモニカの訴訟

4

あなたの本能は正しいです。あなたのクラスは、小さいかもしれませんが、やりすぎです。時限更新キャッシュロジックを完全に汎用的なクラスに分離する必要があります。次に、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のテストが非常に簡単になったことです。


1
リフレッシュロジックが例よりも複雑になった場合は、別のクラスにリファクタリングすることをお勧めします。しかし、例のクラスがそのままではあまりにも多くのことをすることに同意しません。
ドックブラウン

@ kevin、TDDの経験はありません。TimedRefreshCacheをどのようにテストしますか?私はそれを「非常に簡単」とは思わないが、それは私の専門知識の不足かもしれない。
レイヴン

1
私は個人的にあなたの答えが好きではありません。それは複雑だからです。これは非常に一般的で非常に抽象的であり、より複雑な状況で最適な場合があります。しかし、この単純なケースでは「単純に」です。stijnの答えをご覧ください。なんて素敵で、短くて読みやすい答えでしょう。誰もがすぐにそれを理解するでしょう。どう思いますか?
ディーターミームケン

1
@raven短い間隔(100msなど)と非常に単純なプロデューサー(DateTime.Nowなど)を使用して、TimedRefreshCacheをテストできます。キャッシュは100ミリ秒ごとに新しい値を生成し、その間にキャッシュは前の値を返します。
ケビンクライン

1
@DocBrown:問題は、書かれているようにテストできないことです。タイミングロジック(テスト可能)は、データベースロジックと結合されます。データベース呼び出しをモックするためにシームが作成されると、一般的なソリューションへの道の95%になります。これらの小さなクラスの構築は、通常、予想以上に再利用されるため、通常は成果を上げることがわかりました。
ケビンクライン

1

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

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

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

ボブおじさんによると:機能は一つだけすべきだ。彼らはそれをうまくやるべきです。彼らはそれだけを行うべきです。クリーンコードp.35。
カラス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.