3
\$\begingroup\$

Up-to-date version: Countable and uncountable sets in .NET (IEnumerable and Predicate).


Here is my own predicate class; it is equipped with some operators. Demo:

using static BusinessObjects;
using static Console;

class Program
{
    static void Main(string[] args)
    {
        WriteLine(Sell("John Doe", "test@example.com", 10000));
        WriteLine(Sell("John Doe", "test@example.com", 100000));
        WriteLine(Sell("John Doe", "test@example.com", 150000));
    }

    static string Sell(string name, string email, int income)
    {
        if (name == NullOrWhiteSpace || email != ValidEmail)
            throw new Exception("Bad, bad customer.");

        if (income == LowIncome)
            return "Sell car.";

        var middleIncome = !LowIncome && !HighIncome; // magic!!!
        if (income == middleIncome)
            return "Sell home.";

        return "Sell big.";
    }
}

Where business definitions are (verbose, verbose C#):

static class BusinessObjects
{
    public static readonly Condition<string> NullOrEmpty = 
        new Condition<string>(string.IsNullOrEmpty);
    public static readonly Condition<string> NullOrWhiteSpace = 
        new Condition<string>(string.IsNullOrWhiteSpace);

    public static readonly Condition<string> ValidEmail = 
        new Condition<string>(new EmailAddressAttribute().IsValid);

    public static readonly Condition<int> LowIncome = 
        new Condition<int>(i => i < 30000);
    public static readonly Condition<int> HighIncome = 
        new Condition<int>(i => i > 140000);
}

Library class:

public class Condition<T>
{
    public Condition(Predicate<T> predicate)
    {
        Predicate = predicate;
    }

    Predicate<T> Predicate { get; }

    public static bool operator ==(Condition<T> left, T right) =>
        left.Equals(right);

    public static bool operator ==(T left, Condition<T> right) =>
        right.Equals(left);

    public static bool operator !=(Condition<T> left, T right) =>
        !left.Equals(right);

    public static bool operator !=(T left, Condition<T> right) =>
        !right.Equals(left);

    public static bool operator true(Condition<T> x) => false;            

    public static bool operator false(Condition<T> x) => false;

    public static Condition<T> operator |(Condition<T> left, Condition<T> right) =>
        new Condition<T>(v => left.Equals(v) || right.Equals(v));

    public static Condition<T> operator &(Condition<T> left, Condition<T> right) =>
        new Condition<T>(v => left.Equals(v) && right.Equals(v));

    public static Condition<T> operator !(Condition<T> eq) =>
        new Condition<T>(v => !eq.Equals(v));

    public override bool Equals(object obj) =>
        obj is T && Predicate((T)obj);

    public override int GetHashCode()
    {
        throw new NotSupportedException();
    }
}

What do you think? Does this business definitions looks readable and easily reusable?

\$\endgroup\$
  • 3
    \$\begingroup\$ Abusing the equality operator in this way is quite objectionable. I'd go with extension methods. \$\endgroup\$ – Roman Reiner Jun 14 '16 at 5:57
  • \$\begingroup\$ A little bit of "predicate calculus" - it so easy to combine them now, like that mediumIncome. \$\endgroup\$ – Dmitry Nogin Jun 14 '16 at 6:31
13
\$\begingroup\$

Honestly I don't see the advantage of Condition<T> class over a simpler Predicate<T> delegate (eventually with few extension methods to make combinations easier).

Few issues that makes me perplex (at best):

  • You're abusing Equals() to check something that is not equality. This is especially bad because it's absolutely unclear unless users read your Condition<T> implementation.
  • Equality must be commutative and transitive and your actual implementation is not (if "adriano@example.com" == ValidEmail && "dmitry@example.com" == ValidEmail however "adriano@example.com" != "dmitry@example.com").
  • You're throwing NotSupportedException for GetHashCode() however it's used by many .NET Framework classes, what if your users will keep your conditions inside an HashSet?

In short you're adding a class Condition<T> to introduce few handy methods (which will be probably never called in real-world) but with a semantic that will confuse everyone will read your code. Think if button to flush toilet was labeled "mirror light". Confusing, right?

Let's go to your test code, is this:

if (name == NullOrWhiteSpace || email != ValidEmail)
    throw new Exception("Bad, bad customer.");

More clear or shorter than this (using C# 6 using static syntax):

if (IsNullOrWhiteSpace(name) || !IsValidEMail(email))
    throw new ArgumentException("...");

I don't think so and it integrates smoothly into existing code. First principle: don't make your users astonished! You still need some syntactic sugar for combinations:

public static class PredicateExtensions {
    public static Predicate<T> Not(this Predicate<T> rhs) {
        return x => !rhs(x);
    }

    public static Predicate<T> And(this Predicate<T> lhs, Predicate<T> rhs) {
        return x => lhs(x) && rhs(x);
    }

    public static Predicate<T> Nand(this Predicate<T> lhs, Predicate<T> rhs) {
        return x => !lhs(x) && !rhs(x);
    }
}

Used like this:

var middleIncome = HighIncome.Nand(LowIncome); // No magic!

Of course you can drop extension methods if you prefer a plainer syntax:

var middleIncome = Nand(HighIncomem, LowIncome); // Even less magic!

In general what I feel to suggest is to don't try to abuse C# syntax to make your business logic shorter. First of all because shorter code isn't necessarily better (whichever meaning you give to better), if you need a simple (or closer to domain) syntax then create a Domain Specific Language to describe your business logic at high level.


I do not mean that you may not need a Condition<T> class at all. If you're modeling your domain specific constraints in C# it can be pretty handy to have a specific class instead of a generic delegate Predicate<T> but I wouldn't override the comparison operators, in no way a == b can be a ⊢ b.

A possible implementation should also introduce a base non-generic Condition class to allow combinations - if providing required parameters - for conditions with different Ts (this is helpful only if your Condition class also holds its own data.)

\$\endgroup\$
  • \$\begingroup\$ Comments are not for extended discussion; this conversation has been moved to chat. \$\endgroup\$ – janos Jun 14 '16 at 9:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.