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

时间:2011-09-13 04:23:44

标签: c# .net architecture mapping switch-statement

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

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

很大的转换语句smell我已经看到了一些关于如何使用dictionary that maps to functions执行此操作的链接。但我想知道是否有更好的(或更聪明的方法)来做到这一点?在某种程度上,这是一个我一直在脑海中徘徊的问题,但从来没有真正有一个很好的解决方案。

这个问题来自我之前提到的另一个问题:How to select all the values of an object's property on a list of typed objects in .Net with C#

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

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<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;
}

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

10 个答案:

答案 0 :(得分:8)

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

例如,您可以定义静态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>();
}

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

答案 1 :(得分:5)

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

有效地,您现在使用的方法最终会得到如下代码:

var results = GetMyResult(gameInfos, BULLETS_FIRED);

另一种方法是定义一个允许你这样做的扩展方法:

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

这是强类型的,它不需要常量,切换语句,反射或任何神秘的东西。

只需编写这些扩展方法即可:

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()
        };
    }
}

这对你有用吗?

答案 2 :(得分:4)

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

答案 3 :(得分:1)

如果您想管理切换代码,我会指出您设计模式书(GoF),并建议可能会查看Strategy和可能Factory等模式(当我们谈论一般情况下,你的情况不太适合工厂)并实施它们。

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

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

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

答案 4 :(得分:1)

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

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

我将所有属性存储在带有枚举的字典中(更喜欢枚举到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)映射。获取此数据的方法可能会为您确定最佳的映射结构和模式。

答案 5 :(得分:1)

我认为要走的路确实是从某个值(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>

然后,根据请求的统计信息,您总是最终检索一般的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

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

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

答案 6 :(得分:0)

我无法解决您的切换问题,但您可以通过使用可自动映射所需字段的类来减少代码。查看http://automapper.org/

答案 7 :(得分:0)

我不会首先编写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())); 

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

答案 8 :(得分:0)

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

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);
        }

    }
}

答案 9 :(得分:0)

纯粹基于大型switch语句的问题,值得注意的是,Cyclomatic Complexity度量标准有两种常用的变体。 “原始”将每个case语句计为一个分支,因此它将复杂度度量增加1 - 这导致由许多开关引起的非常高的值。 “variant”将switch语句计为单个分支 - 这实际上将其视为一系列非分支语句,这更符合控制复杂性的“可理解性”目标。