web-dev-qa-db-ja.com

クラス内のグローバルな可変変数を回避する方法は?

グローバルの可変変数は誤って変更され、テストが困難になる可能性があるため、悪いことです。ただし、クラスが一部の情報をキャッシュする必要がある場合があります。クラスのパブリック関数に対して最後のリクエストが行われた時間をキャッシュします。この情報は、その関数で特定の決定を行うためにクラスに対して完全にプライベートです。このシナリオでは、クラスは次のようになります。

public class MutableStateClass {
  //some dependencies
  private DateTime lastRequestTime;

  public void SomeFunc() {
    ...
    if(some decision based on lastRequestTime) {
      ...
    }
  }

  //other functions may also access lastRequestTime
}

最後のリクエスト時間を保持するDateTimeパラメーターを持つように関数をパラメーター化できると言うかもしれませんが、この知識はクラスに完全にプライベートであり、このパラメーターをパブリックに導入しても意味がないと思います呼び出し元が最後のリクエスト時間を知る必要がないため、クラスのインターフェース。

別の例としては、いくつかの履歴ポイントに基づいてログに記録するメッセージを決定するブール値の束などがあります。 10秒ごとに実行される可能性のある定期的な機能では、各呼び出しに関する情報をログに記録する必要があり、このログ記録はいくつかの履歴知識に依存する場合があります。この場合も、クラス内のいくつかの関数によって変更できるクラスレベルのブール値で終わる可能性があります。

では、これらの可変のクラスレベル変数をパラメーター化するときにオプションを回避する方法はありますか?これは通常、一部の情報をキャッシュする必要がある場合に発生します。このシナリオのクラスをより適切に設計するにはどうすればよいですか?

グローバル変数に関連する多くの質問があることは知っていますが、ここではクラス内に存在する可変変数について説明したいと思います。アプリケーション全体の可変グローバルについては話していません。

1
Navjot Singh

tl; dr同じクラス内の異なるメソッド間で変更可能な状態を共有する必要がある場合、その変更可能な状態をフィールド/プロパティに保存します完全に合理的なようです。ただし、各メソッドに同じアクセスロジックを複製させるのではなく、その状態の読み取り/変更方法を一元化したい場合があります。これを行うには、単純なディスパッチャーを作成することができます。


同じクラスの異なるメソッドによって変更されているプロパティを持っているようです:

public class SomeObject
{
    private DateTime LastMethodCallTime { get; set ; }

    //  Methods that read/write to the above property:
    public void SomeMethod_001() { /* ... */ }
    public void SomeMethod_002() { /* ... */ }
    public void SomeMethod_003() { /* ... */ }
}

共有された状態を持つことは、それが避けられない場合、実際に悪いことではありません。これは、メソッド呼び出し間で調整が必要な場合(---)、その調整を提供するものが必ず存在する必要があります。その部分は結構です。

しかし、なぜDateTime?各メソッドは、それをチェック/更新するために同じ動作を適切に実装する必要がありますか?もしそうなら、それはおそらく修正するためのものです。

代わりに、次のようにコーディングすることをお勧めします。

public class SomeObject
{
    private DateTime LastMethodCallTime { get; set ; }
    private object ___LOCK___lastMetodCallTimeLockObject { get; } = new object();
    protected void PerformTimeSensitiveAction(Action<DateTime> actionToPerform)
    {
        lock (this.___LOCK___lastMetodCallTimeLockObject)
        {
            var lastMethodCallTime = this.LastMethodCallTime;
            actionToPerform(lastMethodCallTime);
            this.LastMethodCallTime = DateTime.Now;
        }
    }

    //  Methods that call this.PerformTimeSensitiveAction():
    public void SomeMethod_001() { /* ... */ }
    public void SomeMethod_002() { /* ... */ }
    public void SomeMethod_003() { /* ... */ }
}

または、ディスパッチャロジックを分離して、同じクラスに多くなりすぎないようにすることができます。

public class SomeObject
{
    private SimpleDispatcher Dispatcher { get; } = SimpleDispatcher.New();

    //  Methods that call this.Dispatcher:
    public void SomeMethod_001() { /* ... */ }
    public void SomeMethod_002() { /* ... */ }
    public void SomeMethod_003() { /* ... */ }
}

public class SimpleDispatcher
{
    private DateTime LastMethodCallTime { get; set ; }
    private object ___LOCK___lastMetodCallTimeLockObject { get; } = new object();

    protected SimpleDispatcher() { }
    public static SimpleDispatcher New()
    {
        var toReturn = new SimpleDispatcher();
        return toReturn;
    }

    public void Perform(
                Action<DateTime> actionToPerform
        )
    {
        lock (this.___LOCK___lastMetodCallTimeLockObject)
        {
            var lastMethodCallTime = this.LastMethodCallTime;
            actionToPerform(lastMethodCallTime);
            this.LastMethodCallTime = DateTime.Now;
        }
    }
    public T_Result Perform<T_Result>(
                Func<DateTime, T_Result> funcToPerform
        )
    {
        lock (this.___LOCK___lastMetodCallTimeLockObject)
        {
            var lastMethodCallTime = this.LastMethodCallTime;
            var toReturn = funcToPerform(lastMethodCallTime);
            this.LastMethodCallTime = DateTime.Now;

            return toReturn;
        }
    }
}

通常、このようにメソッドのアクセスを制御するオブジェクトはdispatcherと呼ばれます。たとえば、GUIコードを(たとえば、WPFで)記述する場合、ディスパッチャを呼び出してGUIスレッド内でアクションを実行する必要があることがよくあります。

このようなケースでは、ディスパッチャーがどのように機能するかについてあまり気にする必要はありません。その場合、C#-lockで十分です。または、より細かい動作が必要な場合は、ConcurrentQueue<Action>を使用してディスパッチャキューなどを実装できます。

どのような場合でも、オブジェクト全体で変更可能なフィールド/プロパティを共有しているという事実は問題に見えません。なぜなら、異なるメソッド間で状態を共有する必要がある場合、それはそれを置くのにかなり論理的な場所だからです。代わりに、そのオブジェクトで機能するロジックを複製する場合に懸念が生じます。そのため、単純なDateTimeを使用してアクセス方法を複製する代わりに、単純なディスパッチメソッドまたはディスパッチクラスを作成する方がきれいに見えます。

1
Nat

では、これらの可変クラスレベル変数をパラメーター化するときにオプションを回避する方法を教えてください。これは通常、情報をキャッシュする必要がある場合に発生します。このシナリオのクラスをより適切に設計するにはどうすればよいですか?

lastRequestTimeを構築するときに、MutableStateClassを提供および管理する別のクラスを提供できます。

public interface ILastRequestTimeManager {
    DateTime getLastRequestTime();
    setLastRequestTime();
}

public class LastRequestTimeManager : public ILastRequestTimeManager {
    private DateTime lastRequestTime;

    public DateTime getLastRequestTime() {
        return lastRequestTime;
    }
    public setLastRequestTime() {
        lastRequestTime = DateTime.Now();
    }
}

public class MutableStateClass {
    private ILastRequestTimeManager lastRequestTimeMgr;
    public MutableStateClass(ILastRequestTimeManager lastRequestTimeMgr_) : 
        lastRequestTimeMgr(lastRequestTimeMgr_) {}
}

したがって、SRPに従い、実装の詳細はインターフェースの実装に委任されます。

0

呼び出しが行われ、lastRequestTimeが10秒より前である場合に何が起こるか(例として)のようなものに基づいて変化する動作にユニットテストを書き込む必要がある場合は、リクエストにList<DateTime>を使用します単一のプライベートフィールドの代わりに、過去のリクエストに対してオブジェクトのモックデータをインスタンス化できるテスト可視コンストラクタも提供します。

public class MutableStateClass {
  //some dependencies
  private List<DateTime> requestHistory;
  private DateTime getLastRequestTime() => requestHistory.Max(); // needs null-check
  private void appendNewRequestTime() => requestHistory.Add(DateTime.Now);

  public void SomeFunc() {
    ...
    if(some decision based on lastRequestTime) {
      ...
    }
  }

  // make sure only the tests use this
  internal MutableStateClass(List<DateTime> _requestHistory) {
    requestHistory = _requestHistory;
  }

  //other functions may also access lastRequestTime
}
0
Graham

プライベートクラス変数はグローバルではありません。 global変数の問題は、何百万行ものコード内のどこでも読み取りまたは変更できることです。プライベートクラス変数は非常に限定されたスコープでのみ変更できるため、問題はありません。

グローバル変数はユニットテストの一部である必要があります(たとえば、パブリック関数fを呼び出すとグローバル変数xが1増える場合は、ユニットテストでテストする必要があります。プライベートクラス変数の場合は必要ありません。プライベート実装の詳細です。それが関数の動作に影響を与える場合、それは関数の仕様の一部である必要があり、関数がその仕様に従って機能することを確認する単体テストを作成できますが、それはとにかく行う必要があることです。

パブリックメソッドがあるとしましょう

func calledWithinLast10Seconds () -> Bool

その名前のとおりです。単体テストを書くのは簡単ではなく、モックも簡単ではありません。しかし、それは仕様によるものであり、クラス変数を使用して実装するという事実によるものではありません。

0
gnasher729