如何重构这个重复的LINQ代码?

时间:2010-06-03 13:53:06

标签: c# linq refactoring

我试图弄清楚如何很好地重构这个LINQ代码。此代码和其他类似代码在同一文件和其他文件中重复。有时被操纵的数据是相同的,有时数据会发生变化,逻辑也会保持不变。

以下是在不同对象的不同字段上运行的重复逻辑的示例。

public IEnumerable<FooDataItem> GetDataItemsByColor(IEnumerable<BarDto> dtos)
{
    double totalNumber = dtos.Where(x => x.Color != null).Sum(p => p.Number);
    return from stat in dtos
           where stat.Color != null
           group stat by stat.Color into gr
           orderby gr.Sum(p => p.Number) descending
           select new FooDataItem
           {
               Color = gr.Key,
               NumberTotal = gr.Sum(p => p.Number),
               NumberPercentage = gr.Sum(p => p.Number) / totalNumber
           };
}

public IEnumerable<FooDataItem> GetDataItemsByName(IEnumerable<BarDto> dtos)
{
    double totalData = dtos.Where(x => x.Name != null).Sum(v => v.Data);
    return from stat in dtos
           where stat.Name != null
           group stat by stat.Name into gr
           orderby gr.Sum(v => v.Data) descending
           select new FooDataItem
           {
               Name = gr.Key,
               DataTotal = gr.Sum(v => v.Data),
               DataPercentage = gr.Sum(v => v.Data) / totalData
           };
}

任何人都有很好的重构方法吗?

5 个答案:

答案 0 :(得分:11)

这样的事情:

public IEnumerable<FooDataItem> GetDataItems<T>(IEnumerable<BarDto> dtos,
    Func<BarDto, T> groupCriteria,
    Func<BarDto, double> dataSelector,
    Func<T, double, double, FooDataItem> resultFactory)
{
    var validDtos = dtos.Where(d => groupCriteria(d) != null);
    double totalNumber = validDtos.Sum(dataSelector);

    return validDtos
        .GroupBy(groupCriteria)
        .OrderBy(g => g.Sum(dataSelector))
        .Select(gr => resultFactory(gr.Key,
                                    gr.Sum(dataSelector),
                                    gr.Sum(dataSelector) / totalNumber));
}

在您的示例中,您可以这样称呼它:

GetDataItems(
    x => x.Color,  // the grouping criterion
    x => x.Number, // the value criterion
    (key, total, pct) =>
        new FooDataItem {
            Color = key, NumberTotal = total, NumberPercentage = pct });

如果您将FooDataItem更改为更通用,则会更容易。

答案 1 :(得分:3)

我不会使用查询语法,请使用方法链。

public IEnumerable<FooDataItem> GetDataItems(IEnumerable<BarDto> dtos, Func<BarDto, object> key, Func<BarDto, object> data)
{
  double totalData = dtos.Where(d => key(d) != null).Sum(data);
  return dtos.Where(d => key(d) != null)
             .GroupBy(key)
             .OrderBy(d => d.Sum(data))
             .Select(
               o => new FooDataItem() 
               { 
                 Key = o.Key, 
                 Total = o.Sum(data), 
                 Percentage = o.sum(data) / totalData
               });
}

(没有编译器等编写)。

就个人而言,我不会重构它,因为这会使代码更少阅读和理解。

答案 2 :(得分:2)

您需要从查询表达式切换并将所有where,group by,order by和select子句转换为lambdas。然后,您可以创建一个函数,将每个函数作为参数接受。这是一个例子:

private static IEnumerable<FooDataItem> GetData<T>(IEnumerable<Foo> foos, Func<Foo, bool> where, Func<Foo, T> groupby, Func<IGrouping<T, Foo>, T> orderby, Func<IGrouping<T, Foo>, FooDataItem> select)
{
    var query = foos.Where(where).GroupBy(groupby).OrderBy(orderby).Select(select);
    return query;
}

基于此代码

class Foo
{
    public int Id { get; set; }
    public int Bar { get; set; }
}

...

List<Foo> foos = new List<Foo>(); // populate somewhere

Func<Foo, bool> where = f => f.Id > 0;
Func<Foo, int> groupby = f => f.Id;
Func<IGrouping<int, Foo>, int> orderby = g => g.Sum(f => f.Bar);
Func<IGrouping<int, Foo>, FooDataItem> select = g => new FooDataItem { Key = g.Key, BarTotal = g.Sum(f => f.Bar) };

var query = GetData(foos, where, groupby, orderby, select);

答案 3 :(得分:1)

我认为,如果你重构这个,那么阅读比你已经更难阅读。我能想到的任何一件事都涉及动态Linq,或修改或封装BarDto以使某种特殊项目只用于分组。

答案 4 :(得分:1)

这是一个扩展方法,它会将每个查询的相似部分计算出来:

public static IEnumerable<TDataItem> GetDataItems<TData, TDataItem>(
    this IEnumerable<BarDto> dtos,
    Func<BarDto, TData> dataSelector,
    Func<BarDto, double> numberSelector,
    Func<TData, double, double, TDataItem> createDataItem)
    where TData : class
{
    var eligibleDtos = dtos.Where(dto => dataSelector(dto) != null);

    var totalNumber = eligibleDtos.Sum(numberSelector);

    return
        from dto in eligibleDtos
        group dto by dataSelector(dto) into dtoGroup
        let groupNumber = dtoGroup.Sum(numberSelector)
        orderby groupNumber descending
        select createDataItem(dtoGroup.Key, groupNumber, groupNumber / totalNumber);
}

您可以这样使用它:

var itemsByName = dtos.GetDataItems(
    dto => dto.Name,
    dto => dto.Data,
    (name, groupTotal, groupPercentage) => new FooDataItem
    {
        Name = name,
        NumberTotal = groupTotal,
        NumberPercentage = groupPercentage
    });

var itemsByColor = dtos.GetDataItems(
    dto => dto.Color,
    dto => dto.Number,
    (color, groupTotal, groupPercentage) => new FooDataItem
    {
        Color = color,
        DataTotal = groupTotal,
        DataPercentage = groupPercentage
    });