web-dev-qa-db-ja.com

無効な状態のオブジェクトのデザインパターン

オブジェクトのエラー状態の一般的なデザインパターン

ウォレットをモデル化する単純なクラスWalletを考えます。 Walletには一定量のWallet.Cashが含まれており、出金/入金することができます。

    public class Wallet
    {
        /// <summary>
        /// Indicates the amount of Cash in the wallet
        /// </summary>
        public double Cash
        {
            get;
            private set;
        }

        /// <summary>
        /// Takes some money out of the wallet 
        /// </summary>
        public void Spend(double amount)
        {
            Cash -= amount;
        }

        /// <summary>
        /// Puts some money into the wallet
        /// </summary>
        public void Fill(double amount)
        {
            Cash += amount;
        }
    }

特定のオブジェクトの状態が無効です。それらは:Wallet.Cashが負、NaNまたは+/-無限大です。これで、無効なWallet状態への移行が気付かれずに発生しないように、予防策を講じる必要があります。 次のアプローチのうち、「業界標準」はどれですか?

解決策0:チェックを実行しないでください。厳しいフレームワークテストに依存する

(疑わしい?)アプローチは、実装をまったく変更せず、Walletをインスタンス化および管理するテストオブジェクトに依存することです。 nテストの後、コードが適切に機能していると見なします。

このアプローチには、コード行performanceが最適化されるという利点があります。整合性チェックでリソースが無駄になることはありません。

解決策1:オブジェクトが無効な状態になる前に例外をスローする

各メソッドは、操作を実行する前に、操作が無効な状態にならないことを確認できます。上記の例の実装は次のようになります。

    public class Wallet
    {
        /// <summary>
        /// Indicates the amount of Cash in the wallet
        /// </summary>
        public double Cash
        {
            get;
            private set;
        }

        /// <summary>
        /// Takes some money out of the wallet 
        /// </summary>
        public void Spend(double amount)
        {
            if (amount > Cash) throw new Exception("Insufficient cash");

            if (0.0d > amount) throw new Exception("Invalid amount");
            if (Double.IsInfinity(amount)) throw new Exception("Invalid amount");
            if (Double.IsNaN(amount)) throw new Exception("Invalid amount");

            Cash -= amount;
        }

        /// <summary>
        /// Puts some money into the wallet
        /// </summary>
        public void Add(double amount)
        {
            if (0.0d < amount) throw new Exception("You are intending to spend it");

            if (Double.IsInfinity(Cash + amount)) throw new Exception("That's a lot of money in total");
            if (Double.IsNaN(amount)) throw new Exception("Invalid amount");

            Cash += amount;
        }
    }

コード行を事実上2倍以上に増やし、パフォーマンスとコードの保守性に影響を与えます。ただし、利点は、ユーザーがExceptionをキャッチして、Walletがメソッドが呼び出される前の状態であることを認識できるため、失敗した呼び出しが状態を無効にしたかどうかを検討できることです。プロセス全体の。

解決策2:オブジェクトが無効な状態になった後に例外をスローする

各メソッドは、入力パラメーターが有効であると想定し、その役割を果たします。最後に、何かが壊れているかどうかをチェックします。実装は次のようになります

    public class Wallet
    {
        /// <summary>
        /// Indicates the amount of Cash in the wallet
        /// </summary>
        public double Cash
        {
            get;
            private set;
        }

        /// <summary>
        /// Takes some money out of the wallet 
        /// </summary>
        public void Spend(double amount)
        {
            Cash -= amount;

            AssertValidObjectState();
        }

        /// <summary>
        /// Puts some money into the wallet
        /// </summary>
        public void Add(double amount)
        {
            Cash += amount;

            AssertValidObjectState();
        }

        private static Exception exInvalidObjectState = new Exception("[Wallet] Invalid Object State");
        private void AssertValidObjectState()
        {
            if(Double.IsInfinity(Cash)) throw exInvalidObjectState;
            if(Double.IsNaN(Cash)) throw exInvalidObjectState;

            if(0.0d > Cash) throw exInvalidObjectState;
        }
    }

これにより、クラスの最後に小さなスニペットが追加され、各メソッドに1行追加されます。コードの保守性には影響しませんが、オブジェクトの複雑さに応じて、パフォーマンスに影響を及ぼし、不幸な副作用をもたらします。例外がスローされると、プロセスの状態が無効になることを意味します。

解決策3:オブジェクトをエラー状態に設定し、次の読み取りでスローする

最後に、私が見た直交アプローチはExceptionをすぐにスローせず、オブジェクトをエラー状態に設定して初めてスローするread無効な状態から。実装は次のようになります。

    public class Wallet
    {
        /// <summary>
        /// Indicates the amount of Cash in the wallet
        /// </summary>
        public double Cash
        {
            get
            {
                if (null != exInvalidObjectState)
                    throw exInvalidObjectState;

                return cash;
            }
            private set
            {
                cash = value;
            }
        }
        private double cash = 0.0d;

        /// <summary>
        /// Takes some money out of the wallet 
        /// </summary>
        public void Spend(double amount)
        {
            if (null != exInvalidObjectState)
                throw exInvalidObjectState;

            Cash -= amount;

            AssertValidObjectState();
        }

        /// <summary>
        /// Puts some money into the wallet
        /// </summary>
        public void Add(double amount)
        {
            if (null != exInvalidObjectState)
                throw exInvalidObjectState;

            Cash += amount;

            AssertValidObjectState();
        }

        private static Exception exInvalidObjectState = null;
        private void AssertValidObjectState()
        {
            if (Double.IsInfinity(Cash)) exInvalidObjectState = new Exception();
            if (Double.IsNaN(Cash)) exInvalidObjectState = new Exception();

            if (0.0d > Cash) exInvalidObjectState = new Exception();
        }
    }

これは、オブジェクトからデータを取得しない限り、オブジェクトが無効な状態であるかどうかは問題ではないことを意味します。それはデバッグを難しくし、私にとっては間違っているようですが...

3
Benj

クラス間の責任はどうですか?

その質問に対する単一の答えはありません。それは責任の最初の質問です:

  • クラスを使用する前に、操作を実行できるかどうかを確認する必要がありますか?通常、これは、ユーザーに許可されていることについてユーザーに即座にフィードバックを提供するUIクラスにとって意味があります。ただし、一部のチェックが実行されない場合、ソリューション0になる可能性があります。
  • オブジェクトはそれ自体の検証を行う責任がありますか?それがソリューション1です。これにより、コードの信頼性が大幅に向上します。ただし、使用中のオブジェクトがエラーメッセージに反応しない場合は、トランザクションスクリプトが続行され、すべてが正常に行われ、防止された可能性のある不整合が発生する可能性があります。
  • それは、使用しているオブジェクトと使用されているオブジェクトの間の責任を共有する必要がありますか?

上記のいくつかのバリアントもあります:

  • 防御コーディングと不変条件と事後条件を毎回チェックします。しかし、これは生産におけるオーバーヘッドであり、非常に敏感なミッションクリティカルなシステムによってのみ正当化できます。
  • 使用されたオブジェクトが他の場所で矛盾の可能性を示唆するエラーを発生させる場合(たとえば、ウォレットにないお金をとるなど)、例外を使用することを決定できます。

それは、その後一貫して適用する必要がある重要な設計上の決定です。使用中のオブジェクトを想定している使用中のオブジェクトがチェックを実行する一方で、使用中のオブジェクトが有効な操作のみが実行されると想定していることは、何より悪いことではありません。

あなたのソリューションはどうですか?

ソリューション0は時限爆弾です。 errare humanum est以降、遅かれ早かれ、予期しない矛盾した状況が発生する可能性があります。

ソリューション1は問題ありません(上記を参照)。

ソリューション2と3は、適切なステータスが失われ、オブジェクトが使用されなくなったため、遅すぎます。これは、防御コーディングの一部としてのみ妥当です。または、失われる可能性のある値オブジェクトの場合。ソリューション3は浮動小数点の実装で一般的です(つまり、 NaN 状態)。

トランザクション管理

最後に、トランザクションにさまざまなオブジェクトの操作が含まれる場合、全体的な一貫性の問題があります。 1つを除いて、各操作が有効であると想像してください。これらのケースでは、戦略を詳しく説明する必要があります。トランザクションを中断しますか?すでに行った操作を元に戻しますか(それでも可能ですか)?最初にすべてのチェックが実行され、次にすべての操作が実行されますか?

これははるかに複雑であり、ケースバイケースで分析する必要があります。

あなたの例では:

  • Cashが複雑なオブジェクトであり、Spend()を実行するときに最初に触れる場合、ソリューション1でトランザクションを中断するだけで、すべてが正常に行われます。例外が発生すると、ウォレットのコンテキストに渡されます。

  • ウォレットのコンテキストがSpend()を1つのウォレットからAdd()キャッシュから2番目のウォレットに使用する場合、最初のSpend()は成功したがAdd()は失敗します(たとえば、ウォレットにかかる最大現金金額がある場合)?現金を失うので、中断するだけでは不十分です。例外を処理し、最初のウォレットに金額を追加する必要があります。

  • ウォレットが複数のプロセスで同時に対処できる場合はどうなりますか? 1つのトランザクションを実行している間に、別のセッションがウォレットを個別に変更する可能性があります。次に、トランザクションを開始するときにウォレットをロックする必要がありますか?

3
Christophe

いやいやいやいや。

まず、浮動小数点数を使用して基本10の金額を表すのをやめます。ペニーを数える場合は整数は正常に機能し、ドルで表す場合は小数点を忘れずに追加してください。

次に、検証とエラー処理を混同しないでください。例外をスローする必要があるとは何も言われていません。例外は例外的なケースです。すべてがうまくいくとは限らない。

第三に、トランザクションとは何かを学びます。私は財布で今買う余裕がないものを購入しようとしましたが、結果はエラー報告や私の財布を壊す例外であってはなりません。結果は、私は今その物を買うことができないはずではありません。つまり、このトランザクションで提案されたすべての状態変更は、ウォレットの金額が少なすぎるか、ストアの在庫がない場合に解かれる必要があります。トランザクションが発生しなかったこととその理由を報告してください。

例外をスローするウォレットオブジェクトは必要ありません。お金を引き出そうとした結果を伝える1つだけです。

6
candied_orange

通常のオブジェクトアクセス(または「ソリューション1」)に対してスローされた例外は、Python as better for許しよりも許可を求める で造られた一般的なパターンとして知られています。

このパターンは、ウォレットへのアクセスで発生する可能性のあるすべてのエラーをキャッチする責任があるため、ユーザー側では重くなりますが、リソース取得パターンで操作をアトミックにする必要がある場合、これは正しいことです。システム上のファイルを読み取ります(「ファイルが存在するかどうかを確認する」ではなく、ファイルを削除できる場所を「読み取る」のではなく)。

このような種類のシステムレベルの心配がない場合、最善かつ最も標準的なことは、APIコントラクトを再構築してウォレットの一貫性のない状態を許可しないことです。無効なオブジェクト。有効でない場合は、存在しないはずです。

public class Wallet{
    private double cash;
    public double howMuchCanISpend(){
        return cash;
    }
    public void deposit(double amount){
        amount = max(0, amount)
        cash += amount
    }
    public double withdraw(double amount){
        amount = min(max(amount, 0), cash);
        cash -= amount;
        return amount;
    }
}
1
Arthur Havlicek

あなたは「原始的な執着」に苦しんでいると思います。ウォレットに現金の有効な状態の検証コードがあることは、現金を使用する他の場所でも必要になります。 Cashという新しいクラスを作成すると、すべての検証チェックをそこに保持できます。コンストラクターにお勧めします。

このすべての検証を含むCashという名前の不変の値クラスを作成することをお勧めします。

これには、メソッドのシグネチャが誤って現金を表すことを意図していない他のdoubleを渡されないようにするという追加の利点があります。

演算子を記述して使いやすくすることで、最終的にはウォレットクラスで次のようになります。

Cash cashInWallet;

void AddMoney(Cash cash) {
    cashInWallet += cash;
}
1
Adam B

YAGNIを適用したソリューション1:

public class Wallet
    {
        /// <summary>
        /// Indicates the amount of Cash in the wallet
        /// </summary>
        public double Cash
        {
            get;
            private set;
        }

        /// <summary>
        /// Takes some money out of the wallet 
        /// </summary>
        public void Spend(double amount)
        {
            if(Cash - amount < 0) throw new exception("Spending exceeded");

            Cash -= amount;
        }

        /// <summary>
        /// Puts some money into the wallet
        /// </summary>
        public void Add(double amount)
        {
            Cash += amount;
        }
    }

考慮事項:現金は検証を必要とせず、冗長である間に0を追加することは必ずしも悪いシナリオではありません(アカウントのバランスなどを考慮してください)。 doubleは、無限に近づく前に、その容量(フレームワークエラー)を超えます。

1
RandomUs1r