問題の本質
非常に頻繁に、複雑なロジックを開発すると、ネストされたifのツリーが発生します。たとえば、次のようになります。
恐ろしいコード
public double CalculateSomething(Condition condition) { // if(condition.First...) ... // if(condition.Second...) ... // if(condition.AnotherFirst...) { // if(condition.First) ... else... } // if(condition.AnotherSecond...) { // if(condition.Second) ... else... } // if(condition.YetAnotherFirst) { //... if(condition.AnotherFirst && condition.Second) ... else { ... } } // O_o }
それはおなじみですか? ここで問題は何ですか?
問題1:循環的複雑度の増加。 簡単に言えば、循環的複雑さとは、論理演算子を考慮したifとループのネストの深さです。 コード分析ツールを使用すると、コードのすべてのセクションでこのパラメーターを評価できます。 コードの特定のセクションの循環的複雑度のパラメーターは10を超えてはならないと考えられています。この問題により、次のことが大きくなります。
問題2:新しいロジックを追加する。 時間の経過と新しい条件の追加により、新しいロジックを追加する場所と方法を正確に理解することが難しくなります。
問題3:コードの複製。 条件ツリーが分岐している場合、複数の分岐に同じコードが存在する状況を取り除くことができない場合があります。
これが、「ルール」テンプレートの助けになります。 その構造は非常に単純です。
UML図の構造
ここで、Evaluatorクラスには、IRuleインターフェイスの実装のコレクションが含まれています。 評価者はルールを実行し、結果を取得するために使用するルールを決定します。 これがどのように機能し、コードでどのように見えるかを理解するために、C#の小さな例を考えてみましょう。
例。 サイコロゲーム(「タリ」など)
ゲームのルール:
プレーヤーは同時に5つのサイコロを振り、その組み合わせに応じて一定のポイントを受け取ります。
組み合わせは次のとおりです。
1 XXXX
-100ポイント
5 XXXX
-50ポイント
1 1 1 XX
-1000ポイント
2 2 2 XX
-200ポイント
3 3 3 XX
-300ポイント
4 4 4 XX
-400ポイント
5 5 5 XX
-500ポイント
6 6 6 XX
-600ポイント
組み合わせの例:
[1,1,1,5,1]
ポイント
[2,3,4,6,2]
-0ポイント
[3,4,5,3,3]
-350ポイント
もちろん、それらはすべて異なっていてもよく、さらに多くの可能性がありますが、それについては後で詳しく説明します。
一度やってください! パターンなし。
8年生のインフォマティクスレッスンで書くように、「ルール」テンプレートを適用せずにゲームのロジックを説明してみましょう(当然、悪いコードにコメントを提供することなく、誰が必要ですか!)
悪い、邪悪なゲームクラス
public class Game { public int Score(int[] roles) { int score = 0; for(int i=1; i<7; i++) { int count = CountDiceWithValue(roles, i); count = ScoreSetOfN(count, GetSetSize(i), SetSetScore(i), ref score); score += count * GetSingleDieScore(i); } return score } private int GetSingleDieScore(int val) { if(val==1) return 100; if(val==5) return 50; return 0; } private int GetSetScore(int val) { if(val==1) return 1000; return val*100; } private int GetSetSize(int val) { return 3; } private int ScoreSetOfN(int count, int setSize, int setScore, ref int score) { if(count>=setSize) { score += setScore; return count - 3; } return count; } private int CountDiceWithValue(int[] roles, int val) { int count = 0; foreach (int r in roles) { if (r == val) count++; } return count; } }
二回やる! ルールを追加しますか? 単体テスト。
50行のコードは非常に小さいようです。 しかし、ゲームのルールが変更されて追加された場合はどうなりますか?
たとえば、サイコロのさまざまな組み合わせのルールを追加します。
1 1 1 1 X
-2000
1 1 1 1 1
1-4000
1 2 3 4 5
5-8000
2 3 4 5 6
6-8000
AABBX
-4000
などなど。
この場合、コードは非常に混乱するリスクがあります。 これを避けるために、「ルール」テンプレートを使用してコードを書き直します。
(ここで、リファクタリングする前に、すべてのケースをユニットテストでカバーし、それらの重要性とコードリファクタリングの必要性についてつぶやく必要があると言ってください)
3つやって! 「ルール」テンプレートを適用する
1.
Eval
メソッドを使用して
IRule
インターフェイスを定義します。これは、特定のサイコロセットのポイント数を推定するために必要です。
IRule.cs
public interface IRule { ScoreResult Eval(int[] dice); }
2.ルールセット、ルールを追加するロジック、およびこのキューブセットに適用できる最適なルールを選択するロジックを決定するクラス
RuleSet
作成します。
RuleSet.cs
public class RuleSet { // private List<IRule> _rules = new List<IRule>(); // public void Add(IRule rule) { _rules.Add(rule); } // - , public IRule BestRule(int[] dice) { ScoreResult bestResult = new ScoreResult(); foreach(var rule in _rules) { var result = rule.Eval(dice); if(result.Score > bestResult.Score) { bestResult = result; } return bestResult.RuleUsed; } } }
3.もちろん、小さなヘルパークラス
ScoreResult.cs
public class ScoreResult { // public int Score {get;set;} // ( ) public int[] DiceUsed {get;set;} // , , ( BestRule) public IRule RuleUsed {get;set;} }
4.ルール自体を定義します。
ConcreteRules.cs
// public class SingleDieRule : IRule { private readonly int _value; private readonly int _score; public SingleDieRule(int dieValue, int score) { _dieValue = dieValue, _score = score } // - public ScoreResult Eval(int[] dice) { //- var result = new ScoreResult(); // ( ) - result.DiceUsed = dice.Where(d=>d == dieValue).ToArray(); // result.Score = result.DiceUsed.Count() * _score; // - result.RuleUsed = this; return result; } } //
5.この場合、スキームの
Evaluator
クラスは
Game
クラスになり、ルールを追加するためのロジックとスコアリングのためのロジックを除いて、ほとんど何も含まれません。
Game.cs-評価者
public class Game { private readonly RuleSet _ruleSet = new RuleSet(); public Game(bool useAllRules) { // _ruleSet.Add(new SingleDieRule(1,100)); _ruleSet.Add(new SingleDieRule(5,50)); _ruleSet.Add(new TripleDieRule(1,1000)); for(int i=2; i<7; i++) { _ruleSet.Add(new TripleDieRule(i, i*100)); } // if(useAllRules) { _ruleSet.Add(new FourOfADieRule(1,2000)); _ruleSet.Add(new SetOfADieRule(5,1,4000)); _ruleSet.Add(new StraightRule(8000)); _ruleSet.Add(new TwoPairsRule(6000)); for(int i=2; i<7; i++) { _ruleSet.Add(new FourOfADieRule(i,i*200)); _ruleSet.Add(new SetOfADieRule(i,i*400)); //... } } } // public void AddScoringRule(IRule rule) { _ruleSet.Add(rule); } // public int Score(int[] dice) { int score = 0; var remainingDice = new List<int>(dice); var bestRule = _ruleSet.BestRule(remainingDice.ToArray()); // while(bestRule!=null) { var result = bestRule.Eval(remainingDice.ToArray()); foreach(var die in result.DiceUsed) { remainingDice.Remove(die); } score+=result.Score; bestRule = _ruleSet.BestRule(remainingDice.ToArray()); } return score; } }
やった! 問題は解決しました! 現在、各クラスは本来あるべき姿に取り組んでおり、循環的複雑性は増大していません。
新しいルールが簡単かつ簡単に追加されます。 これで、一連のルールを含む
RuleSet
クラスを使用してルールが
RuleSet
、ルールが追加され、
Game
クラスによってスコアリングされます。
何を覚えておく必要がありますか?
ルールベースのロジックを含むプログラムを設計する場合、次の問題に留意することが役立ちます。
-状態を変更しないように、システムに対してルールを読み取り専用にする必要がありますか?
-ルール間に依存関係があるべきですか? あるルールが機能するために別のルールの結果を必要とする可能性がある場合、ルールが実行される順序に注意を払う価値がありますか。
-ルールの施行順序を厳密に定義する必要がありますか?
-ルールの実装には優先順位がありますか?
-エンドユーザーにルールの編集を許可する必要がありますか?
その他多数。
ビジネスルールエンジンについて一言
ビジネスルールエンジンの概念は、「ルール」テンプレートの概念に非常に似ています。これらは、
ビジネスロジックのルールシステムを定義します。 通常、それらには特定のグラフィカルインターフェイスがあり、ユーザーがデータベースまたはファイルシステムに格納できるルールとルールの階層を定義できます。 特に、MicrosoftのWorkflow Foundationにもこの機能があります。
まとめ
1)条件とブランチの複雑さを取り除く必要がある場合は、「ルール」テンプレートを使用します
2)各ルールのロジックとその効果をクラスに入れます
3)別のクラスでルールの選択と処理を分離する-評価者
4)ビジネスロジック用の既製の「エンジン」ソリューションがあることを知っています。
ご清聴ありがとうございました。このトレーニング資料の私の創造的な改訂が誰かを助けることを願っています。
*この記事のインスピレーションの源は、 Steve Smithによるmultiplesight.comの「Design Patterns」コースの「Rules Pattern」レッスンでした。