Как отрефакторить это дублирование кода LINQ?
Я пытаюсь выяснить, как рефакторинг этого кода 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 ответов:
Что-то вроде этого:
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 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);
Я думаю, что если бы вы перефразировали это, было бы труднее читать, чем то, что вы уже имеете. Все, что я могу придумать, либо включает динамический Linq, либо модифицирует или инкапсулирует BarDto, чтобы иметь какой-то специализированный элемент, который будет использоваться только для группировки.
Вот метод расширения, который учитывает аналогичные части каждого запроса:
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 });
Comments