从体系结构上讲,我应该如何用更易于管理的语句替换一个非常大的switch语句
本文关键字:语句 替换 switch 非常 一个 管理 结构上 我应该 易于 何用更 | 更新日期: 2023-09-27 18:08:29
EDIT 1:忘记添加嵌套属性曲线球。
UPDATE:我选择了@mtazva的答案,因为这是我具体情况的首选解决方案。回想起来,我用一个非常具体的例子问了一个一般的问题,我相信最终会让每个人(或者可能只有我)困惑,不知道这个问题到底是什么。我相信一般的问题也已经得到了回答(参见策略模式的答案和链接)。谢谢大家!
大的switch语句明显有异味,我看过一些关于如何使用映射到函数的字典来做到这一点的链接。但我想知道是否有更好(或更聪明)的方法来做到这一点?在某种程度上,这是一个我一直在脑海中盘旋的问题,但从来没有一个好的解决方案。
这个问题源于我之前问过的另一个问题:如何使用c#在。net的类型化对象列表中选择对象's属性的所有值
下面是我正在使用的一个示例类(来自外部源):
public class NestedGameInfoObject
{
public string NestedName { get; set; }
public int NestedIntValue { get; set; }
public decimal NestedDecimalValue { get; set; }
}
public class GameInfo
{
public int UserId { get; set; }
public int MatchesWon { get; set; }
public long BulletsFired { get; set; }
public string LastLevelVisited { get; set; }
public NestedGameInfoObject SuperCoolNestedGameInfo { get; set; }
// thousands more of these
}
不幸的是,这是来自外部来源…想象一下《侠盗猎车手》之类的大数据转储。
我想要得到这些物体列表的一个小截面。想象一下,我们希望能够将你与好友的游戏信息对象进行比较。单个用户的单独结果如下所示:
public class MyResult
{
public int UserId { get; set; } // user id from above object
public string ResultValue { get; set; } // one of the value fields from above with .ToString() executed on it
}
和一个我想用更易于管理的东西替换的例子(相信我,我不想维护这个怪物switch语句):
const int MATCHES_WON = 1;
const int BULLETS_FIRED = 2;
const int NESTED_INT = 3;
public static List<MyResult> GetMyResult(GameInfo[] gameInfos, int input)
{
var output = new List<MyResult>();
switch(input)
{
case MATCHES_WON:
output = gameInfos.Select(x => new MyResult()
{
UserId = x.UserId,
ResultValue = x.MatchesWon.ToString()
}).ToList<MyResult>();
break;
case BULLETS_FIRED:
output = gameInfos.Select(x => new MyResult()
{
UserId = x.UserId,
ResultValue = x.BulletsFired.ToString()
}).ToList<MyResult>();
break;
case NESTED_INT:
output = gameInfos.Select(x => new MyResult()
{
UserId = x.UserId,
ResultValue = x.SuperCoolNestedGameInfo.NestedIntValue.ToString()
}).ToList<MyResult>();
break;
// ad nauseum
}
return output;
}
所以问题是有什么合理的方法来管理这个野兽吗?我真正想要的是在初始对象发生变化的情况下获得这些信息的动态方式(例如,添加更多游戏信息属性)。有没有更好的方法来构建它,让它不那么笨拙?
我认为你的第一句话回避了可能是最合理的解决方案:某种形式的字典将值映射到方法。
例如,您可以定义一个静态Dictionary<int, func<GameInfo, string>>
,其中每个值(如MATCHES_WON)都将添加一个相应的lambda,该lambda提取适当的值(假设您的常量等定义如示例中所示):
private static Dictionary<int, Func<GameInfo, string>> valueExtractors =
new Dictionary<int, Func<GameInfo, string>>() {
{MATCHES_WON, gi => gi.MatchesWon.ToString()},
{BULLETS_FIRED, gi => gi.BulletsFired.ToString()},
//.... etc for all value extractions
};
然后,您可以使用此字典提取示例方法中的值:
public static List<MyResult> GetMyResult(GameInfo[] gameInfos, int input)
{
return gameInfo.Select(gi => new MyResult()
{
UserId = gi.UserId,
ResultValue = valueExtractors[input](gi)
}).ToList<MyResult>();
}
在此选项之外,您可能会对数字和属性名称进行某种文件/数据库/存储查找,然后使用反射来提取值,但这显然不会执行得很好。
我觉得这段代码有点失控了。您正在有效地使用常量来索引属性——这会创建脆弱的代码,您希望使用一些技术(例如反射、字典等)来控制增加的复杂性。
实际上,你现在使用的方法最终会得到这样的代码:
var results = GetMyResult(gameInfos, BULLETS_FIRED);
另一种方法是定义一个扩展方法,让您可以这样做:
var results = gameInfos.ToMyResults(gi => gi.BulletsFired);
这是强类型的,它不需要常量、switch语句、反射或任何晦涩的东西。
只需编写这些扩展方法,您就完成了:
public static class GameInfoEx
{
public static IEnumerable<MyResult> ToMyResults(
this IEnumerable<GameInfo> gameInfos,
Func<GameInfo, object> selector)
{
return gameInfos.Select(gi => gi.ToMyResult(selector));
}
public static MyResult ToMyResult(
this GameInfo gameInfo,
Func<GameInfo, object> selector)
{
return new MyResult()
{
UserId = gameInfo.UserId,
ResultValue = selector(gameInfo).ToString()
};
}
}
这对你有用吗?
您可以使用反射来实现这些目的。您可以实现自定义属性,标记您的属性等。此外,如果你的类发生了变化,它也是一种动态的获取信息的方式。
如果你想管理交换机代码,我建议你参考设计模式书(GoF),并建议你参考Strategy
和Factory
这样的模式(这是我们讨论一般案例使用的时候,你的案例不太适合Factory)并实现它们。
虽然在重构模式完成后,switch语句仍然必须留在某个地方(例如,在按id选择策略的地方),但代码将更加易于维护和清晰。
说到一般的交换机维护,如果它们变得像野兽一样,我不确定这是最好的解决方案,因为你的情况看起来很相似。
我100%肯定您可以创建一些方法(可能是扩展方法),将接受所需的属性访问器lambda,应该在生成结果时使用。
如果您希望您的代码更通用,我同意使用字典或某种查找模式的建议。
可以将函数存储在字典中,但它们似乎都执行相同的操作——从属性中获取值。这是一个值得反思的时机。
我将所有属性存储在一个字典中,以enum(首选enum而不是const
)作为键,并以PropertyInfo(或者,不太优选的,描述属性名称的字符串)作为值。然后调用PropertyInfo对象上的GetValue()
方法从对象/类中检索值。
下面是一个示例,我将枚举值映射到类中的"同名"属性,然后使用反射从类中检索值。
public enum Properties
{
A,
B
}
public class Test
{
public string A { get; set; }
public int B { get; set; }
}
static void Main()
{
var test = new Test() { A = "A value", B = 100 };
var lookup = new Dictionary<Properties, System.Reflection.PropertyInfo>();
var properties = typeof(Test).GetProperties().ToList();
foreach (var property in properties)
{
Properties propertyKey;
if (Enum.TryParse(property.Name, out propertyKey))
{
lookup.Add(propertyKey, property);
}
}
Console.WriteLine("A is " + lookup[Properties.A].GetValue(test, null));
Console.WriteLine("B is " + lookup[Properties.B].GetValue(test, null));
}
你可以将你的const
值映射到属性的名称,与这些属性相关的PropertyInfo对象,将检索属性值的函数…只要你认为适合你的需要。
当然您将需要一些映射 -在此过程中,您将依赖于您的输入值(const
)映射到特定属性。获取此数据的方法可能会为您确定最佳的映射结构和模式。
我认为要走的路确实是某种从一个值(int)到某种知道如何提取值的函数的映射。如果你真的想保持它的可扩展性,这样你就可以很容易地添加一些,而不触及代码,并可能访问更复杂的属性(如。嵌套属性,做一些基本的计算),你可能想把它放在一个单独的源代码中。
我认为这样做的一种方法是依赖于脚本服务,例如评估一个简单的IronPython表达式来提取一个值…
例如,在文件中可以存储如下内容:
<GameStats>
<GameStat name="MatchesWon" id="1">
<Expression>
currentGameInfo.BulletsFired.ToString()
</Expression>
</GameStat>
<GameStat name="FancyStat" id="2">
<Expression>
currentGameInfo.SuperCoolNestedGameInfo.NestedIntValue.ToString()
</Expression>
</GameStat>
</GameStats>
,然后,根据请求的状态,你总是最终检索一般的gameinfo。你可以使用foreach循环:
foreach( var gameInfo in gameInfos){
var currentGameInfo = gameInfo
//evaluate the expression for this currentGameInfo
return yield resultOfEvaluation
}
参见http://www.voidspace.org.uk/ironpython/dlr_hosting.shtml获取如何在。net应用程序中嵌入IronPython脚本的示例。
注意:当处理这类东西时,有几件事你必须非常小心:
- 这可能会让别人在你的应用程序中注入代码…
- 你应该在这里测量动态评估的性能影响
对于您的开关问题,我没有一个解决方案,但是您肯定可以通过使用一个可以自动映射您需要的所有字段的类来减少代码。查看http://automapper.org/.
我一开始就不会编写GetMyResult
方法。它所做的就是将GameInfo
序列转换为MyResult
序列。用Linq来做会更容易,也更有表现力。
而不是调用
var myResultSequence = GetMyResult(gameInfo, MatchesWon);
我只调用
var myResultSequence = gameInfo.Select(x => new MyResult() {
UserId = x.UserId,
ResultValue = x.MatchesWon.ToString()
});
为了更简洁,你可以在构造函数
中传递UserId
和ResultValue
var myResultSequence =
gameInfo.Select(x => new MyResult(x.UserId, x.MatchesWon.ToString()));
只有当您看到选择被重复得太多时才进行重构。
这是一种不使用反射的方法:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1
{
public class GameInfo
{
public int UserId { get; set; }
public int MatchesWon { get; set; }
public long BulletsFired { get; set; }
public string LastLevelVisited { get; set; }
// thousands more of these
}
public class MyResult
{
public int UserId { get; set; } // user id from above object
public string ResultValue { get; set; } // one of the value fields from above with .ToString() executed on it
}
public enum DataType
{
MatchesWon = 1,
BulletsFired = 2,
// add more as needed
}
class Program
{
private static Dictionary<DataType, Func<GameInfo, object>> getDataFuncs
= new Dictionary<DataType, Func<GameInfo, object>>
{
{ DataType.MatchesWon, info => info.MatchesWon },
{ DataType.BulletsFired, info => info.BulletsFired },
// add more as needed
};
public static IEnumerable<MyResult> GetMyResult(GameInfo[] gameInfos, DataType input)
{
var getDataFunc = getDataFuncs[input];
return gameInfos.Select(info => new MyResult()
{
UserId = info.UserId,
ResultValue = getDataFunc(info).ToString()
});
}
static void Main(string[] args)
{
var testData = new GameInfo[] {
new GameInfo { UserId="a", BulletsFired = 99, MatchesWon = 2 },
new GameInfo { UserId="b", BulletsFired = 0, MatchesWon = 0 },
};
// you can now easily select whatever data you need, in a type-safe manner
var dataToGet = DataType.MatchesWon;
var results = GetMyResult(testData, dataToGet);
}
}
}
纯粹就大型switch语句的问题而言,值得注意的是,通常使用的圈复杂度度量有两种变体。"原始"将每个case语句计数为一个分支,因此它将复杂性度量增加1 -这导致由许多切换引起的非常高的值。"变体"将switch语句视为单个分支——这有效地将其视为非分支语句的序列,这更符合控制复杂性的"可理解性"目标。