嵌套切换语句:架构设计问题

时间:2013-05-13 21:18:47

标签: c#

我正在开发一个项目,我的任务是添加一个高级搜索和过滤选项,允许用户通过指定尽可能多的条件从Windows事件列表中查询所需结果想。

这个想法是每个Windows事件日志都有几个属性,例如LogNameSourceCreatedDateMessageNumber等。(部分属性 FieldItem 枚举)。总共有四种可能的数据类型:StringDateTimeIntegral (Int/Long)EventEntryType。这四种数据类型中的每一种都有自己的选择器操作数集合( SelectorOperator 枚举的一部分)。这是一张图片,可以让您更好地了解整体结构的样子:

我最初实现这个想法的是:

 public static class SearchProvider
{
    public static List<EventLogItem> SearchInLogs(List<EventLogItem> currentLogs, SearchQuery query)
    {
        switch (query.JoinType)
        {
            case ConditionJoinType.All:
                return SearchAll(currentLogs, query);
            case ConditionJoinType.Any:
                return SearchAny(currentLogs, query);
            default:
                return null;
        }
    }

    private static List<EventLogItem> SearchAll(List<EventLogItem> currentLogs, SearchQuery query)
    {
        foreach (SearchCondition condition in query.Conditions)
        {
            switch (condition.FieldName)
            {
                case FieldItem.Category:
                    switch (condition.SelectorOperator)
                    {
                        case SelectorOperator.Contains:
                            currentLogs = currentLogs.Where(item => item.Category.ToLower().Contains(condition.FieldValue as string)).ToList();
                            break;
                        case SelectorOperator.EndsWith:
                            currentLogs = currentLogs.Where(item => item.Category.ToLower().EndsWith(condition.FieldValue as string)).ToList();
                            break;
                        case SelectorOperator.Is:
                            currentLogs = currentLogs.Where(item => string.Equals(item.Category, condition.FieldValue as string, StringComparison.OrdinalIgnoreCase)).ToList();
                            break;
                        case SelectorOperator.StartsWith:
                            currentLogs = currentLogs.Where(item => item.Category.ToLower().StartsWith(condition.FieldValue as string)).ToList();
                            break;
                    }
                    break;
                case FieldItem.InstanceID:
                    switch (condition.SelectorOperator)
                    {
                        case SelectorOperator.Equals:
                            currentLogs = currentLogs.Where(item => item.InstanceID == long.Parse(condition.FieldValue as string)).ToList();
                            break;
                        case SelectorOperator.IsGreaterThan:
                            currentLogs = currentLogs.Where(item => item.InstanceID > long.Parse(condition.FieldValue as string)).ToList();
                            break;
                        case SelectorOperator.IsLessThan:
                            currentLogs = currentLogs.Where(item => item.InstanceID < long.Parse(condition.FieldValue as string)).ToList();
                            break;
                    }
                    break;
                case FieldItem.LogName:
                    switch (condition.SelectorOperator)
                    {
                        case SelectorOperator.Contains:
                            currentLogs = currentLogs.Where(item => item.LogName.ToLower().Contains(condition.FieldValue as string)).ToList();
                            break;
                        case SelectorOperator.EndsWith:
                            currentLogs = currentLogs.Where(item => item.LogName.ToLower().EndsWith(condition.FieldValue as string)).ToList();
                            break;
                        case SelectorOperator.Is:
                            currentLogs = currentLogs.Where(item => string.Equals(item.LogName, condition.FieldValue as string, StringComparison.OrdinalIgnoreCase)).ToList();
                            break;
                        case SelectorOperator.StartsWith:
                            currentLogs = currentLogs.Where(item => item.LogName.ToLower().StartsWith(condition.FieldValue as string)).ToList();
                            break;
                    }
                    break;
                case FieldItem.Message:
                    switch (condition.SelectorOperator)
                    {
                        case SelectorOperator.Contains:
                            currentLogs = currentLogs.Where(item => item.Message.ToLower().Contains(condition.FieldValue as string)).ToList();
                            break;
                        case SelectorOperator.EndsWith:
                            currentLogs = currentLogs.Where(item => item.Message.ToLower().EndsWith(condition.FieldValue as string)).ToList();
                            break;
                        case SelectorOperator.Is:
                            currentLogs = currentLogs.Where(item => string.Equals(item.Message, condition.FieldValue as string, StringComparison.OrdinalIgnoreCase)).ToList();
                            break;
                        case SelectorOperator.StartsWith:
                            currentLogs = currentLogs.Where(item => item.Message.ToLower().StartsWith(condition.FieldValue as string)).ToList();
                            break;
                    }
                    break;
                case FieldItem.Number:
                    switch (condition.SelectorOperator)
                    {
                        case SelectorOperator.Equals:
                            currentLogs = currentLogs.Where(item => item.Number == int.Parse(condition.FieldValue as string)).ToList();
                            break;
                        case SelectorOperator.IsGreaterThan:
                            currentLogs = currentLogs.Where(item => item.Number > int.Parse(condition.FieldValue as string)).ToList();
                            break;
                        case SelectorOperator.IsLessThan:
                            currentLogs = currentLogs.Where(item => item.Number < int.Parse(condition.FieldValue as string)).ToList();
                            break;
                    }
                    break;
                case FieldItem.Source:
                    switch (condition.SelectorOperator)
                    {
                        case SelectorOperator.Contains:
                            currentLogs = currentLogs.Where(item => item.Source.ToLower().Contains(condition.FieldValue as string)).ToList();
                            break;
                        case SelectorOperator.EndsWith:
                            currentLogs = currentLogs.Where(item => item.Source.ToLower().EndsWith(condition.FieldValue as string)).ToList();
                            break;
                        case SelectorOperator.Is:
                            currentLogs = currentLogs.Where(item => string.Equals(item.Source, condition.FieldValue as string, StringComparison.OrdinalIgnoreCase)).ToList();
                            break;
                        case SelectorOperator.StartsWith:
                            currentLogs = currentLogs.Where(item => item.Source.ToLower().StartsWith(condition.FieldValue as string)).ToList();
                            break;
                    }
                    break;
                case FieldItem.Type:
                    switch (condition.SelectorOperator)
                    {
                        case SelectorOperator.Is:
                            currentLogs = currentLogs.Where(item => item.Type == (EventLogEntryType)Enum.Parse(typeof(EventLogEntryType), condition.FieldValue as string)).ToList();
                            break;
                        case SelectorOperator.IsNot:
                            currentLogs = currentLogs.Where(item => item.Type != (EventLogEntryType)Enum.Parse(typeof(EventLogEntryType), condition.FieldValue as string)).ToList();
                            break;
                    }
                    break;
            }
        }

        return currentLogs;
    }

示例查询可能如下所示:

条件选择器:

All of the conditions are met

条件

LogName Is "Application"
Message Contains "error"
Type IsNot "Information"
InstanceID IsLessThan 1934

正如您所看到的,由于嵌套的SearchAll()语句,switch方法很长且不易维护。但是,代码有效,我觉得这不是实现此设计的最优雅方式。有没有更好的方法来解决这个问题?也许通过找出一种方法来降低switch层次结构的复杂性或使代码更通用?任何帮助/建议表示赞赏。

4 个答案:

答案 0 :(得分:2)

处理此类任务的标准方法是创建自定义IQueryable提供程序并使用LINQ。从字面上看,您正在寻找的每个操作都通过LINQ表达式具有标准的可扩展性机制。基本的想法是,你应该ExpressionVisitor个实现应用每个重写规则而不是一个巨大的switch语句。由于您可以根据需要使用尽可能多的表达式访问者,因此维护和可扩展性成本会下降。

如果您想采用这种方法,我强烈建议您查看IQToolkit和Matt Warren的Building an IQueryable博客系列。

答案 1 :(得分:2)

避免嵌套和相关复制的一种方法是将提取值的代码部分与对其执行操作的代码部分分开。这是一个应该说明技术的小例子:

Func<EventLogEntry,string> getString = null;
Func<EventLogEntry,int> getInt32 = null;
...
switch (condition.FieldName) {
    case FieldItem.Category: getString = e => e.Category; break;
    case FieldItem.Message:  getString = e => e.Message;  break;
    case FieldItem.Number:   getInt32  = e => e.Number;   break;
    default:                 throw new ApplicationException("Unsupported field");
}
switch (condition.SelectorOperator) {
   case SelectorOperator.Contains:
        currentLogs = currentLogs.Where(item => getString(item).ToLower().Contains(condition.FieldValue as string)).ToList();
   break;
   case SelectorOperator.EndsWith:
       currentLogs = currentLogs.Where(item => getString(item).ToLower().EndsWith(condition.FieldValue as string)).ToList();
   break;
   case SelectorOperator.Is:
       currentLogs = currentLogs.Where(item => string.Equals(getString(item), condition.FieldValue as string, StringComparison.OrdinalIgnoreCase)).ToList();
   break;
   case SelectorOperator.StartsWith:
       currentLogs = currentLogs.Where(item => getString(item).ToLower().StartsWith(condition.FieldValue as string)).ToList();
   break;
   case SelectorOperator.Equals:
       currentLogs = currentLogs.Where(item => getInt32(item) == int.Parse(condition.FieldValue as string)).ToList();
   break;
   case SelectorOperator.IsGreaterThan:
       currentLogs = currentLogs.Where(item => getInt32(item) > int.Parse(condition.FieldValue as string)).ToList();
   break;
   case SelectorOperator.IsLessThan:
       currentLogs = currentLogs.Where(item => getInt32(item) < int.Parse(condition.FieldValue as string)).ToList();
   break;
}

现在添加新的源字段需要向第一个case添加另一个switch;向类型添加新操作只需要在第二个case中添加一个新switch,从而减​​少代码中“维护点”的数量。

答案 2 :(得分:1)

我认为你确实需要两个switch语句,但它们不需要嵌套。您可以将操作分离到任何类型的对象上,然后在运行时传入要搜索的对象。

public static class SearchProvider
{
    static Func<object, bool> GetSearchMethod(SelectorOperator selectorOperator, string conditionFieldValue)
    {
        switch (selectorOperator)
        {
            //strings
            case SelectorOperator.Contains:
                return new Func<object, bool>(s => s.ToString().ToLower().Contains(conditionFieldValue));
            case SelectorOperator.StartsWith:
                return new Func<object, bool>(s => s.ToString().ToLower().StartsWith(conditionFieldValue));
            case SelectorOperator.EndsWith:
                return new Func<object, bool>(s => s.ToString().ToLower().EndsWith(conditionFieldValue));
            case SelectorOperator.Is:
                return new Func<object, bool>(s => string.Equals(s.ToString(), conditionFieldValue, StringComparison.OrdinalIgnoreCase));

            //numbers
            case SelectorOperator.Equals:
                return new Func<object, bool>(n => (long)n == long.Parse(conditionFieldValue));
            case SelectorOperator.IsGreaterThan:
                return new Func<object, bool>(n => (long)n > long.Parse(conditionFieldValue));
            case SelectorOperator.IsLessThan:
                return new Func<object, bool>(n => (long)n < long.Parse(conditionFieldValue));

            //type
            case SelectorOperator.TypeIs:
                return new Func<object, bool>(t => (EventLogEntryType)t == (EventLogEntryType)Enum.Parse(typeof(EventLogEntryType), conditionFieldValue));
            case SelectorOperator.TypeIsNot:
                return new Func<object, bool>(t => (EventLogEntryType)t != (EventLogEntryType)Enum.Parse(typeof(EventLogEntryType), conditionFieldValue));

            default:
                throw new Exception("Unknown selector operator");
        }
    }

    private static List<EventLogItem> SearchAll(List<EventLogItem> currentLogs, SearchQuery query)
    {
        foreach (SearchCondition condition in query.Conditions)
        {
            var search = GetSearchMethod(condition.SelectorOperator, condition.FieldValue as string);
            switch (condition.FieldName)
            {
                case FieldItem.Category:
                    currentLogs = currentLogs.Where(item => search(item.Category)).ToList();
                    break;
                case FieldItem.InstanceID:
                    currentLogs = currentLogs.Where(item => search(item.InstanceID)).ToList();
                    break;
                case FieldItem.LogName:
                    currentLogs = currentLogs.Where(item => search(item.LogName)).ToList();
                    break;
                case FieldItem.Message:
                    currentLogs = currentLogs.Where(item => search(item.Message)).ToList();
                    break;
                case FieldItem.Number:
                    currentLogs = currentLogs.Where(item => search(item.Number)).ToList();
                    break;
                case FieldItem.Source:
                    currentLogs = currentLogs.Where(item => search(item.Source)).ToList();
                    break;
                case FieldItem.Type:
                    currentLogs = currentLogs.Where(item => search(item.Type)).ToList();
                    break;
            }
        }
        return currentLogs;
    }
}

注意我发布这个晚了因为SO服务器崩溃了,然后我去睡觉了:(
因此它类似于@dasblinkenlight的回答。

答案 3 :(得分:1)

我不明白为什么人们建议采用IQueryable方法。我一直认为IQueryable用于将C#查询转换为SQL(SELECT语句)或XML(XQuery)等其他技术中的查询,因此可以在适当的位置执行,而无需了解技术的任何细节。查询转换为(由您作为开发人员/程序员或您的代码 - 与该技术没有紧密耦合)。

由于您的查询是在C#/ .NET代码中执行的,因此不需要IQueryable。

例如,如果您使用EventLog服务的本机查询功能,那么实现IQueryable以将C#LINQ转换为查询字符串或EventLog服务理解和执行的其他形式将非常棒。

对我来说,这个问题看起来像是通过链接谓词来创建复合谓词的问题,因此复合谓词可以在LINQ Where语句中使用。

这取决于您希望解决方案的通用程度,但这里有一个可能的实现,它大量使用类型推断和lambda闭包来创建复合谓词:

class Predicate<T>
{
    public static Func<T, bool> Or(params Func<T, bool>[] predicates)
    {
        return item => predicates.Any(p => p(item));
    }

    public static Func<T, bool> And(params Func<T, bool>[] predicates)
    {
        return item => predicates.All(p => p(item));
    }

    #region Generic predicates

    public static Func<T, bool> Is<TValue>(Func<T, TValue> selector, string value) where TValue : IEquatable<TValue>
    {
        return item => GetEqualityComparer<TValue>().Equals(selector(item), Parse<TValue>(value));
    }

    public static Func<T, bool> IsNot<TValue>(Func<T, TValue> selector, string value) where TValue : IEquatable<TValue>
    {
        return item => !Is(selector, value)(item);
    }

    public static Func<T, bool> IsLessThan<TValue>(Func<T, TValue> selector, string value) where TValue : IComparable<TValue>
    {
        return item => GetComparer<TValue>().Compare(selector(item), Parse<TValue>(value)) < 0;
    }

    public static Func<T, bool> IsLessThanOrEqualTo<TValue>(Func<T, TValue> selector, string value) where TValue : IComparable<TValue>
    {
        return item => GetComparer<TValue>().Compare(selector(item), Parse<TValue>(value)) <= 0;
    }

    public static Func<T, bool> IsGreaterThan<TValue>(Func<T, TValue> selector, string value) where TValue : IComparable<TValue>
    {
        return item => !IsLessThanOrEqualTo(selector, value)(item);
    }

    public static Func<T, bool> IsGreaterThanOrEqualTo<TValue>(Func<T, TValue> selector, string value) where TValue : IComparable<TValue>
    {
        return item => !IsLessThan(selector, value)(item);
    }

    public static Func<T, bool> IsBetween<TValue>(Func<T, TValue> selector, string lower, string higher) where TValue : IComparable<TValue>
    {
        return item => IsGreaterThan(selector, lower)(item) && IsLessThan(selector, higher)(item);
    }

    #endregion

    #region String specialized predicates

    public static Func<T, bool> Contains(Func<T, string> selector, string value)
    {
        return item => selector(item).IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0;
    }

    public static Func<T, bool> StartsWith(Func<T, string> selector, string value)
    {
        return item => selector(item).StartsWith(value, StringComparison.OrdinalIgnoreCase);
    }

    public static Func<T, bool> EndsWith(Func<T, string> selector, string value)
    {
        return item => selector(item).EndsWith(value, StringComparison.OrdinalIgnoreCase);
    }

    #endregion

    private static IEqualityComparer<TValue> GetEqualityComparer<TValue>()
    {
        // If value type is string, use OrdinalIgnoreCase equality comparer.
        return typeof(TValue) == typeof(string) ? (IEqualityComparer<TValue>)StringComparer.OrdinalIgnoreCase : EqualityComparer<TValue>.Default;
    }

    private static IComparer<TValue> GetComparer<TValue>()
    {
        // If value type is string, use OrdinalIgnoreCase comparer.
        return typeof(TValue) == typeof(string) ? (IComparer<TValue>)StringComparer.OrdinalIgnoreCase : Comparer<TValue>.Default;
    }

    private static TValue Parse<TValue>(string value)
    {
        // We need special handling for Enum type since, unfortunately, System.String doesn't handle conversion to Enum type in its IConvertible.ToType implementation.
        // All other used types (string, DateTime, int, long) are supported by Convert class.
        return (TValue)(typeof(TValue).IsEnum ? Enum.Parse(typeof(TValue), value) : Convert.ChangeType(value, typeof(TValue), CultureInfo.InvariantCulture));
    }
}

// For easier typing, no need to explicitly specify type.
class EventLogPredicate : Predicate<EventLogItem>
{
}

以下是您可以使用它的方法:

var items = new List<EventLogItem>()
{
    new EventLogItem() { LogName = "First" },
    new EventLogItem() { LogName = "Second bla", Number = 100 },
    new EventLogItem() { LogName = "Third bla", Number = 25 },
    new EventLogItem() { LogName = "Fourth", Number = 25 }
};

var predicate = EventLogPredicate.And(EventLogPredicate.Contains(item => item.LogName, "bla"), EventLogPredicate.IsLessThan(item => item.Number, "50"));

var filteredItems = items.Where(predicate).ToArray();