从架构上讲,我应该如何用更易于管理的东西替换一个非常大的switch语句?

编辑1:忘记添加嵌套属性曲线球。

更新:我选择了@ mtazva的答案,因为这是我特定案例的首选解决方案。 回想起来,我用一个非常具体的例子问了一个普遍的问题,我认为这最终会使每个人(或者也许只是我)混淆这个问题究竟是什么。 我相信一般性问题也得到了回答(参见战略模式答案和链接)。 感谢大家!

很大的开关语句显然有气味 ,我已经看到了一些关于如何使用映射到函数的字典来实现这一点的链接。 但我想知道是否有更好的(或更聪明的方法)来做到这一点? 在某种程度上,这是一个我总是在脑后滚动的问题,但从来没有真正有一个很好的解决方案。

这个问题源于我之前提到的另一个问题: 如何使用C#在.Net中的类型对象列表中选择对象属性的所有值

这是我正在使用的示例类(来自外部源):

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 } 

我想用更易于管理的东西替换的一个例子(相信我,我不想维护这个怪物切换语句):

 const int MATCHES_WON = 1; const int BULLETS_FIRED = 2; const int NESTED_INT = 3; public static List GetMyResult(GameInfo[] gameInfos, int input) { var output = new List(); switch(input) { case MATCHES_WON: output = gameInfos.Select(x => new MyResult() { UserId = x.UserId, ResultValue = x.MatchesWon.ToString() }).ToList(); break; case BULLETS_FIRED: output = gameInfos.Select(x => new MyResult() { UserId = x.UserId, ResultValue = x.BulletsFired.ToString() }).ToList(); break; case NESTED_INT: output = gameInfos.Select(x => new MyResult() { UserId = x.UserId, ResultValue = x.SuperCoolNestedGameInfo.NestedIntValue.ToString() }).ToList(); break; // ad nauseum } return output; } 

所以问题是有没有合理的方法来管理这个野兽? 我真正喜欢的是在初始对象发生变化(例如,添加更多游戏信息属性)的情况下获取此信息的动态方式。 有没有更好的方法来设计它,所以它不那么笨拙?

我认为你的第一句话可能是最合理的解决方案:某种forms的字典映射值到方法。

例如,您可以定义一个静态Dictionary> ,其中每个值(如MATCHES_WON)都将添加一个相应的lambda,该lambda将提取相应的值(假设您的常量等定义如下所示)例):

 private static Dictionary> valueExtractors = new Dictionary>() { {MATCHES_WON, gi => gi.MatchesWon.ToString()}, {BULLETS_FIRED, gi => gi.BulletsFired.ToString()}, //.... etc for all value extractions }; 

然后,您可以使用此字典提取示例方法中的值:

 public static List GetMyResult(GameInfo[] gameInfos, int input) { return gameInfo.Select(gi => new MyResult() { UserId = gi.UserId, ResultValue = valueExtractors[input](gi) }).ToList(); } 

在此选项之外,您可能会使用数字和属性名称进行某种文件/数据库/存储查找,然后使用reflection来提取值,但这显然不会执行。

我认为这段代码已经失控了。 您正在有效地使用常量来索引属性 – 这就是创建脆弱的代码,您希望使用某些技术 – 例如 – reflection,字典等 – 来控制增加的复杂性。

实际上,您现在使用的方法最终会得到如下代码:

 var results = GetMyResult(gameInfos, BULLETS_FIRED); 

另一种方法是定义一个允许您执行此操作的扩展方法:

 var results = gameInfos.ToMyResults(gi => gi.BulletsFired); 

这是强类型的,它不需要常量,switch语句,reflection或任何神秘的东西。

只需编写这些扩展方法,您就完成了:

 public static class GameInfoEx { public static IEnumerable ToMyResults( this IEnumerable gameInfos, Func selector) { return gameInfos.Select(gi => gi.ToMyResult(selector)); } public static MyResult ToMyResult( this GameInfo gameInfo, Func selector) { return new MyResult() { UserId = gameInfo.UserId, ResultValue = selector(gameInfo).ToString() }; } } 

那对你有用吗?

您可以将reflection用于这些目的。 您可以实现自定义属性,标记您的属性等。此外,它是动态的方式来获取有关您的类的信息,如果它更改。

如果你想管理开关代码,我会指向你设计模式书(GoF)并建议你可能看看像Strategy和可能的Factory这样的模式(当我们谈论一般情况下使用时,你的情况不适合工厂)和实施它们。

虽然在重构模式完成之后仍然需要将switch语句留在某处(例如,在您通过id选择策略的地方),代码将更加可维护和清晰。

关于一般的开关维护说,如果它们变成野兽,我不确定它的最佳解决方案,因为你的案例陈述看起来有多么相似。

我100%确定你可以创建一些方法(可能是一个扩展方法)来接受所需的属性访问器lambda,这应该在生成结果时使用。

如果您希望您的代码更通用,我同意字典或某种查找模式的建议。

您可以将函数存储在字典中,但它们似乎都执行相同的操作 – 从属性中获取值。 这是成熟的反思。

我将所有属性存储在一个字典中,其中包含一个枚举(更喜欢枚举为const )作为键,以及一个PropertyInfo – 或者不太优选的是一个描述属性名称的字符串 – 作为值。 然后,您可以在PropertyInfo对象上调用GetValue()方法,以从对象/类中检索值。

这是一个示例,我将枚举值映射到类中的“相同命名”属性,然后使用reflection从类中检索值。

 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(); 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表达式来提取值…

例如,在文件中,您可以存储以下内容:

    currentGameInfo.BulletsFired.ToString()     currentGameInfo.SuperCoolNestedGameInfo.NestedIntValue.ToString()    

然后,根据请求的统计数据,您总是最终检索一般的GameInfos。 他们可以使用某种foreach循环:

 foreach( var gameInfo in gameInfos){ var currentGameInfo = gameInfo //evaluate the expression for this currentGameInfo return yield resultOfEvaluation } 

有关如何在.NET应用程序中嵌入IronPython脚本的示例,请参阅http://www.voidspace.org.uk/ironpython/dlr_hosting.shtml 。

注意:使用这种东西时,有几件事你必须要小心:

  • 这可能允许某人在您的应用程序中注入代码……
  • 您应该在此处衡量动态评估的性能影响

我没有解决你的开关问题,但你可以通过使用一个可以自动映射你需要的所有字段的类来减少代码。 查看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() }); 

为了使它更简洁,您可以在构造函数中传递UserIdResultValue

  var myResultSequence = gameInfo.Select(x => new MyResult(x.UserId, x.MatchesWon.ToString())); 

仅当您看到选项变得过多时才重构。

这是不使用reflection的一种可能方式:

 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> getDataFuncs = new Dictionary> { { DataType.MatchesWon, info => info.MatchesWon }, { DataType.BulletsFired, info => info.BulletsFired }, // add more as needed }; public static IEnumerable 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语句的问题,值得注意的是,Cyclomatic Complexity度量标准有两种常用的变体。 “原始”将每个case语句计为一个分支,因此它将复杂度度量增加1 – 这导致由许多开关引起的非常高的值。 “variant”将switch语句计为单个分支 – 这实际上将其视为一系列非分支语句,这更符合控制复杂性的“可理解性”目标。