TellDontAsk principal sais:
Tell-Don't-Ask is a principle that helps people remember that object-orientation is about bundling data with the functions that operate on that data. It reminds us that rather than asking an object for data and acting on that data, we should instead tell an object what to do. This encourages to move behavior into an object to go with the data.
Will IComparer<T>
implementation work for you instead of exposing rules collection? Let’s say we have:
public class Person
{
public static readonly SortingParser<Person> Sorting =
new SortingParser<Person>()
.OrderBy(p => p.Age)
.OrderBy(p => p.City, StringComparer.InvariantCultureIgnoreCase)
.OrderBy(p => p.Name);
public string Name { get; set; }
public int Age { get; set; }
public string City { get; set; }
}
Then we can get IComparer<T>
this way:
IComparer<Person> comparer;
Person.Sorting.TryParse("+Name;+Age;-City", out comparer);
Where:
public class SortingParser<T>
{
public SortingParser()
: this(ImmutableDictionary<string, IComparer<T>>.Empty)
{
}
SortingParser(ImmutableDictionary<string, IComparer<T>> comparers)
{
Comparers = comparers;
}
public SortingParser<T> OrderBy<V>(
Expression<Func<T, V>> selector) =>
OrderBy(selector, Comparer<V>.Default);
public SortingParser<T> OrderBy<V>(
Expression<Func<T, V>> selector, IComparer<V> comparer) =>
new SortingParser<T>(
Comparers.Add(
((MemberExpression)selector.Body).Member.Name.ToLower(),
new RelayComparer<T,V>(selector.Compile(), comparer)));
public bool TryParse(string expression, out IComparer<T> comparer)
{
comparer = CompositeComparer<T>.Empty;
if (expression == null)
return false;
IComparer<T> ruleComparer;
foreach (var rule in expression.Split(';'))
{
if (!Comparers.TryGetValue(
rule.TrimStart('+', '-').Trim().ToLower(),
out ruleComparer))
{
comparer = null;
return false;
}
switch (rule[0])
{
case '+':
comparer = comparer.ThenBy(ruleComparer);
break;
case '-':
comparer = comparer.ThenByDescending(ruleComparer);
break;
default:
comparer = null;
return false;
}
}
return true;
}
ImmutableDictionary<string, IComparer<T>> Comparers { get; }
}
I use System.Collections.Immutable
NuGet package and these helper library classes:
public static class ComparerComposition
{
public static IComparer<T> Invert<T>(this IComparer<T> comparer) =>
new InvertedComparer<T>(comparer);
public static IComparer<T> ThenBy<T>(
this IComparer<T> first, IComparer<T> second) =>
new CompositeComparer<T>(first, second);
public static IComparer<T> ThenByDescending<T>(
this IComparer<T> first, IComparer<T> second) =>
new CompositeComparer<T>(first, second.Invert());
}
And
public class InvertedComparer<T> : IComparer<T>
{
public InvertedComparer(IComparer<T> inner)
{
Inner = inner;
}
public int Compare(T x, T y) =>
Inner.Compare(y, x);
IComparer<T> Inner { get; }
}
And:
public class CompositeComparer<T> : IComparer<T>
{
public static readonly IComparer<T> Empty = new CompositeComparer<T>();
public CompositeComparer(params IComparer<T>[] comparers)
{
Comparers = comparers;
}
public int Compare(T x, T y) =>
Comparers
.Select(c => c.Compare(x, y))
.FirstOrDefault(r => r != 0);
IComparer<T>[] Comparers { get; }
}
And:
public class RelayComparer<T, V> : IComparer<T>
{
public RelayComparer(Func<T,V> selector, IComparer<V> parent)
{
Selector = selector;
Parent = parent;
}
public int Compare(T x, T y) =>
Parent.Compare(Selector(x), Selector(y));
Func<T, V> Selector { get; }
IComparer<V> Parent { get; }
}