Я пытаюсь выяснить, как осуществить рефакторинг этот код 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
};
}
У кого-либо есть хороший способ осуществить рефакторинг это?
Что-то вроде этого:
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
на более общий, это будет проще.
Вот метод расширения, который выделяет аналогичные части каждого запроса:
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
});
Я думаю, что если бы вы отредактировали это, было бы труднее читать, чем то, что у вас уже есть. Все, что я могу придумать, связано либо с динамическим Linq, либо с изменением или инкапсуляцией BarDto, чтобы иметь какой-то специализированный элемент, который можно использовать только для группировки.
Я бы не стал использовать для этого синтаксис запроса, используйте цепочку методов.
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
});
}
(написано без компилятора и т. Д.).
Лично я бы не стал его реорганизовывать, так как это сделает код менее читаемым и понятным.
Вам нужно будет переключиться с выражения запроса и преобразовать все ваши предложения where, group by, order by и select в лямбда-выражения. Затем вы можете создать функцию, которая принимает каждый из них в качестве параметров. Вот пример:
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);