次のコード例があります
if(object.Time > 0 && <= 499)
{
rate = .75m
}
else if(object.Time >= 500 && <= 999)
{
rate = .85m
}
else if(object.Time >= 1000)
{
rate = 1.00m
}
else
{
rate = 0m;
}
私の質問は、これを改善するためにどのデザインパターンを使用できるかということです。
編集:もう少し明確にするために、ここに表示されるコードは、戦略パターンの実装内に現在存在するものです。 3種類の計算があり、そのうち2つには、以下に示す時間に基づいて使用できる3つの異なる「レート」があります。レートごとに戦略の実装を作成することを考えましたが、使用する戦略を決定するためのロジックを移動し、それも混乱させます。
ありがとう!
あなたが本当にデザインパターンを探しているなら、私はChainofResponsibilityパターンを選びます。
基本的に、「リンク」は入力を処理しようとします。処理できない場合は、他のリンクが処理できるようになるまでチェーンに渡されます。ユニットテストがある場合は、ユニットテストで簡単にモックするためのインターフェイスを定義することもできます。
したがって、すべてのリンクが継承するこの抽象クラスがあります。
public abstract class Link
{
private Link nextLink;
public void SetSuccessor(Link next)
{
nextLink = next;
}
public virtual decimal Execute(int time)
{
if (nextLink != null)
{
return nextLink.Execute(time);
}
return 0;
}
}
次に、ルールを使用して各リンクを作成します。
public class FirstLink : Link
{
public override decimal Execute(int time)
{
if (time > 0 && time <= 499)
{
return .75m;
}
return base.Execute(time);
}
}
public class SecondLink : Link
{
public override decimal Execute(int time)
{
if (time > 500 && time <= 999)
{
return .85m;
}
return base.Execute(time);
}
}
public class ThirdLink : Link
{
public override decimal Execute(int time)
{
if (time >= 1000)
{
return 1.00m;
}
return base.Execute(time);
}
}
最後に、それを使用するには、すべての後継者を設定して呼び出します。
Link chain = new FirstLink();
Link secondLink = new SecondLink();
Link thirdLink = new ThirdLink();
chain.SetSuccessor(secondLink);
secondLink.SetSuccessor(thirdLink);
そして、あなたがしなければならないのは、1回のクリーンコールでチェーンを呼び出すことだけです。
var result = chain.Execute(object.Time);
'ルールパターン'というあまり有名ではないパターンがあります
すべてがオブジェクトに抽出されるおよび独自のジョブを処理するという考え方です。 各ルールの各クラス定義した条件ステートメントがあります。例: (object.Time> 0 && <= 499)
public class RuleNumberOne : IRules
{
public decimal Execute(Oobject date)
{
if(date.Time > 0 && date.Something <= 499)
return .75m;
return 0;
}
}
public class RuleNumberTwo : IRules
{
public decimal Execute(Oobject date)
{
if(date.Time >= 500 && date.Something <= 999)
return .85m;
return 0;
}
}
public interface IRules
{
decimal Execute(Oobject date);
}
したがって、以前はこのように見えていたクラスでは
if(object.Time > 0 && <= 499)
{
rate = .75m
}
else if(object.Time >= 500 && <= 999)
{
rate = .85m
}
else if(object.Time >= 1000)
{
rate = 1.00m
}
else
{
rate = 0m;
}
なります、
private List<IRules>_rules = new List<IRules>();
public SomeConstructor()
{
_rules.Add(new RuleNumberOne());
_rules.Add(new RuleNumberTwo());
}
public void DoSomething()
{
Oobject date = new Oobject();
foreach(var rule in this._rules)
{
Decimal rate = rule.Execute(date);
}
}
ここでの考え方は、if条件をネストすると、条件ステートメントを読むのが難しくなり、開発者が変更を加えるのが難しくなるということです。したがって、個々のルールのロジックとその効果を、ルールの単一責任パターンに従う独自のクラスに分離します。
いくつかの考慮事項は 1.)読み取り専用2.)明示的な順序3.)依存関係4.)優先度5.)永続性
繰り返しになりますが、条件の複雑さが増し、アプリケーションの要件がそれを保証する場合は、ルールパターンの使用を検討してください。
小数などを返したくない場合はカスタマイズできますが、アイデアはここにあります。
範囲の1つのエンドポイントのみをチェックする必要があります。もう1つは、以前の条件が偽だったため、コード内のその時点に実際にいることを意味します。
if (obj.Time <= 0) {
rate = 0.00m;
}
// At this point, obj.Time already must be >= 0, because the test
// to see if it was <= 0 returned false.
else if (obj.Time < 500) {
rate = 0.75m;
}
// And at this point, obj.Time already must be >= 500.
else if (obj.Time < 1000) {
rate = 0.85m;
}
else {
rate = 1.00m;
}
読みやすさとパフォーマンスの理由から、スケールのより一般的な端を最初にチェックするものにすることをお勧めします。しかし、どちらの方法でも機能します。
if-else
条件では、デザインパターンではなくフォーマットを選択できます。
一般に、lot of conditions
がある場合はネストされたif-else
よりもif
の方が好きです。次のようなものを選ぶことができます。
if(condition1){
return x; // or some operation
}
if(condition 2){
return y; // or some operation
}
return default; // if none of the case is satisfied.
私はLeoLorenzoLuisのソリューションが本当に好きです。しかし、レートを返す代わりに、ルールに何かをさせます。これは、 S.O.L.I.D。Principles および Law Of Demeter からのSを尊重します。
また、クラスが別のクラスに含まれている値を「要求」する場合、それを データクラス と呼ばれるsmell
として識別できます。これは避けてください。
そうは言っても、私はLeoLorenzoのソリューションを磨くために2つのことをします。
for loop
なしで右のRule
を呼び出します。Rule
クラス内で要求された動作を実行します。これを行うには、rule
クラスを時間範囲にマップする必要があります。これにより、ループを繰り返す代わりに、直接アクセスできるようになります。独自のマップオブジェクト(またはリストオブジェクト、またはコレクション)を実装し、[] operator
とそれがadd
関数をオーバーロードする必要があります。
たとえば、次のようにマップにルールを追加できます。
ranges.Add(0,500).AddRule(rule1);
ranges.Add(500,1000).AddRule(rule2);
etc..
上記のように、オブジェクトRange
を関連付けることができるオブジェクトRule
があります。したがって、最終的には同じRange
に複数のルールを追加できます。
次に、次のように呼び出します。
ranges[Object.time].Execute(Object);
マップの使用:
var map = new[]
{
new { Rule = (Func<Oobject, bool>) ( x => x.Time > 0 && x.Something <= 499 ),
Value = .75m },
new { Rule = (Func<Oobject, bool>) ( x => x.Time >= 500 && x.Something <= 999 ),
Value = .85m },
new { Rule = (Func<Oobject, bool>) ( x => true ),
Value = 0m }
};
var date = new Oobject { Time = 1, Something = 1 };
var rate = map.First(x => x.Rule(date) ).Value;
Assert.That( rate, Is.EqualTo(.75m));
@lllのRules Pattern
回答のアイデアは気に入っていますが、欠点があります。
次のテスト(NUnit)について考えてみます。
[Test]
public void TestRulesUsingList()
var rules = new IRules[]{ new RuleNumberOne(), new RuleNumberTwo() };
var date = new Oobject { Time = 1, Something = 1 };
var rate = 0m;
foreach(var rule in rules)
rate = rule.Execute(date);
Assert.That( rate, Is.EqualTo(.75m));
}
テストは失敗します。 RuleNumberOne
が呼び出されてゼロ以外の値が返されましたが、その後RuleNumberTwo
が呼び出されてゼロが返され、正しい値が上書きされました。
If..else..elseロジックを複製するには、短絡できる必要があります。
簡単な修正方法は次のとおりです。インターフェイスのExecute
メソッドを変更してbool
を返し、ルールを実行するかどうかを示し、Value
プロパティを追加してルールのdecimal
値を取得します。また、alwasysがtrue
を評価し、ゼロを返すdefulatルールを追加します。次に、実装(テスト)を変更して、trueと評価する最初のルールの値を取得します。
[Test]
public void TestRulesUsingList2()
{
var rules = new IRules[]{ new RuleNumberOne(), new RuleNumberTwo(),
new DefaultRule() };
var date = new Oobject { Time = 1, Something = 1 };
var rate = rules.First(x => x.Execute(date)).Value;
Assert.That( rate, Is.EqualTo(.75m));
}
public class Oobject
{
public int Time { get; set; }
public int Something { get; set; }
}
public interface IRules
{
bool Execute(Oobject date);
decimal Value { get; }
}
public class RuleNumberOne : IRules
{
public bool Execute(Oobject date)
{
return date.Time > 0 && date.Something <= 499;
}
public decimal Value
{
get { return .75m; }
}
}
public class RuleNumberTwo : IRules
{
public bool Execute(Oobject date)
{
return date.Time >= 500 && date.Something <= 999;
}
public decimal Value
{
get { return .85m; }
}
}
public class DefaultRule : IRules
{
public bool Execute(Oobject date)
{
return true;
}
public decimal Value
{
get { return 0; }
}
}
大量の「if」がある場合、またはこの情報を設定ファイルに入れたい場合は、この情報を格納するクラスを作成することをお勧めします。
Class
FromTime
ToTime
Value
values.Add(New Class(0, 499, .75));
values.Add(New Class(500, 999, .85));
values.Add(New Class(1000, 9999, 1));
次に、コレクション内の各アイテムをループします
if(object.Time >= curItem.FromTime && object.Time <= curItem.ToTime)
rate = curItem.Value;
常にnull許容値を設定するか、-1を無限大として設定できます。
values.Add(New Class(-1, 0, 0));
values.Add(New Class(0, 499, .75));
values.Add(New Class(500, 999, .85));
values.Add(New Class(1000, -1, 1));
if((object.Time >= curItem.FromTime || curItem.FromTime == -1) && (object.Time <= curItem.ToTime || curItem.ToTime == -1))
rate = curItem.Value;
Ifごとに1つの比較を行い、値を上から下に移動します。
if (Object.Time >= 1000)
rate = 1.0;
else
if (Object.Time >= 500)
rate = 0.85;
else
if (Object.Time > 0)
rate = 0.75;
else
rate = 0;
これはアンチパターンの問題ではないと思います。コードメトリクスの場合も問題ありません。 Ifはネストされておらず、それほど複雑ではありません!。ただし、たとえばSwitchを使用して改善したり、IsAgeBiggerThanMax()などのプロパティを含む独自のクラスを作成したりできます。
スイッチの更新:
var range = (time - 1) / 499;
switch (range)
{
case 0: // 1..499
rate = 0.75;
break;
case 1: // 500.. 999
rate = 0.85;
break;
default:
rate = 0;
if (time == 1000)
{
rate = 1.0;
}
break;
}
テストは哲学の質問であり、この機能が何で、何が行われているのかわかりません。多分それは外部から100%テストすることができます!