Showing posts with label encapsulation. Show all posts
Showing posts with label encapsulation. Show all posts

Wednesday, June 22, 2016

FuncComparer

Often we need a means to compare collections of complex structures. Consider



public class Contact
{
    public Guid Id { get; private set; }
    public string Name { get; set; }
    public Contact() { Id = Guid.NewGuid(); }
}

[TestMethod]
public void Test()
{
    Contact[] expected = new[] 
    {
        new Contact { Name = "Bob", },
        new Contact { Name = "Abel", },
    };
    Contact[] actual = new[] 
    {
        new Contact { Name = "Abel", }, 
        new Contact { Name = "Charlie", },
    };
    // TODO: test for equivalence?
}


I often run into situations like these when testing functions on collections, as presented above. I also see similar situations when comparing collections from a client (that may have modified or net-new entities) to collections from a datastore.


There are many ways to resolve these sorts of scenarios, like in-lining comparison logic, overriding Equals method, or even declaring a custom equivalence class that implements IEqualityComparer<Contact>. The first method is brittle and completely not portable. Overriding Equals is a little more portable, but still fairly brittle - for instance, if in one context I require equality based on Name, in another context equality of Id, and yet another based on both. The third method is portable and is externalized from the class so that it may be clearly swapped depending on context (consider ContactNameEqualityComparer versus ContactIdEqualityComparer).


My only gripe with this last method is that it is always so tedious to implement the boiler-plate comparisons in every instance of a comparer (for instance, writing trivial accept and reject criteria for ContactNameEqualityComparer, and then re-writing that criteria again for ContactIdEqualityComparer, and then again for any other comparers that exist in our system for other types).


FuncComparer



Really I want something that encapsulates basic comparison functionality while being able to supply comparison criteria as lambdas.


public class FuncComparer<T> : IComparer, IComparer<T>
{

    private readonly Comparison<T>[] comparisons = null;
    private readonly int xIsNotNullComparison = 0;
    private readonly int yIsNotNullComparison = 0;

    public FuncComparer(
        Comparison<T> comparison, 
        bool isNullLessThan = true) :
        this (new[] { comparison, }, isNullLessThan)
    {
    }

    public FuncComparer(
        Comparison<T>[] comparisons, 
        bool isNullLessThan = true)
    {
        this.comparisons = comparisons;
        this.xIsNotNullComparison = isNullLessThan ? 1 : -1;
        this.yIsNotNullComparison = isNullLessThan ? -1 : 1;
    }

    // interfaces

    #region IComparer Members

    public int Compare(object x, object y)
    {
        int comparison = 0;
        if (!ReferenceEquals(x, y))
        {
            bool xIsNull = true;
            bool yIsNull = true;
            T a = default(T);
            T b = default(T);
            if (x is T)
            {
                xIsNull = false;
                a = (T)(x);
            }
            if (y is T)
            {
                yIsNull = false;
                b = (T)(y);
            }
            comparison = Compare(a, xIsNull, b, yIsNull);
        }
        return comparison;
    }

    #endregion

    #region IComparer<T> Members

    public int Compare(T x, T y)
    {
        int comparison = 0;
        if (!ReferenceEquals(x, y))
        {
            comparison = Compare(x, x == null, y, y == null);
        }
        return comparison;
    }

    #endregion

    // private methods

    /// <summary>
    /// Compare two strong-typed values. Explicit 'is null' 
    /// parameters are required for scenarios where 
    /// <typeparamref name="T"/> is a valuetype and type-less 
    /// compare is used. In such cases, type-less method uses 
    /// default-type value which will telescope nulls to default
    /// values. For example, when comparing a series of integers,
    /// any nulls in our inputs will be interpreted as '0'.
    /// </summary>
    /// <param name="x"></param>
    /// <param name="isXNull"></param>
    /// <param name="y"></param>
    /// <param name="isYNull"></param>
    /// <returns></returns>
    private int Compare(T x, bool isXNull, T y, bool isYNull)
    {
        int comparison = 0;
        if (!isXNull && !isYNull)
        {
            for (
                int i = 0; 
                i < comparisons.Length && comparison == 0; 
                i++)
            {
                comparison = comparisons[i](x, y);
            }
        }
        else if (!isXNull)
        {
            comparison = xIsNotNullComparison;
        }
        else if (!isYNull)
        {
            comparison = yIsNotNullComparison;
        }
        // both x and y are null, implies equality, no-op
        //else
        //{
        //}
        return comparison;
    }

}

What we may now do is
public class Contact
{
    public Guid Id { get; private set; }
    public string Name { get; set; }
    public Contact() { Id = Guid.NewGuid(); }
}

[TestMethod]
public void Test()
{
    FuncComparer<Contact> contactNameComparer = 
        new FuncComparer<Contact>(
            (a,b) => string.Compare(
                a.Name,
                b.Name,
                StringComparison.InvariantCultureIgnoreCase));
    Contact[] expected = new[] 
    {
        new Contact { Name = "Bob", },
        new Contact { Name = "Abel", },
    };
    Contact[] actual = new[] 
    {
        new Contact { Name = "Abel", }, 
        new Contact { Name = "Charlie", },
    };
    // NOTE: successfully demonstrates ease-of-use, esp
    // when tests are repeated with minor variances in
    // expectation
    CollectionAssert.AreNotEqual(
        expected, 
        actual, 
        contactNameComparer);
    CollectionAssert.AreEqual(
        new[] 
        {
            new Contact { Name = "Abel", },
            new Contact { Name = "Charlie", },
        },
        actual,
        contactNameComparer);
}


Why 'isXNull' and 'isYNull'?



The oddest thing about FuncComparer is how it handles null checks. We perform null checks on the way in, before the private core-implementation executes. This is because the private core-implementation is strongly typed, co-ercing any null inputs into base-type defaults. For reference types, such as any class, this is ok, since default(T) is still null. However, for valuetypes like int or struct, we end up telescoping nulls into default values, which is a loss-full transformation, a reduction in fidelity.


We overcome this through explicit null tests on the way in, and pass that on as state to our core-implementation.



[TestMethod]
public void Test()
{
    IComparer typelessComparer = 
        new FuncComparer<int>(
            (a,b) => a.CompareTo(b),
            true);
    // we expect 'null is less-than', so x as null should
    // return less-than
    int expectedComparison = -1;
    int actualComparison = 
        typelessComparer.Compare(null, 0);
    Assert.AreEqual(expectedComparison, actualComparison);
}

One last minor detail, is the ability to configure an ordinal interpretation of null in relation to all other values. Our constructor accepts a boolean that indicates any null values should be considered less-than non-null values, or vice versa. This allows us to tweak things like list ordering.
[TestMethod]
public void Test()
{
    IComparer typelessComparer = 
        new FuncComparer<int>(
            (a,b) => a.CompareTo(b),
            false);
    object[] expected = new object[] { 0, 1, 2, 3, 4, null, };
    ArrayList actual = new ArrayList { 1, null, 4, 2, 3, 0, };
    actual.Sort(typelessComparer);
    CollectionAssert.AreEqual(expected, actual);
}

FuncEqualityComparer

Upon implementing a FuncComparer, the very next thing that I do is implement a FuncEqualityComparer; so it follows that I should post this next. An implementation of IEqualityComparer on top of an IComparer is relatively trivial and straightforward,


public class FuncEqualityComparer<T> : 
    FuncComparer<T>, 
    IEqualityComparer, 
    IEqualityComparer<T>
{

    private readonly Func<T, int> getHashCode = null;

    public FuncEqualityComparer(
        Comparison<T> comparison, 
        Func<T, int> getHashCode, 
        bool isNullLessThan = true) :
        this(new[] { comparison, }, getHashCode, isNullLessThan)
    {
    }

    public FuncEqualityComparer(
        Comparison<T>[] comparisons, 
        Func<T, int> getHashCode, 
        bool isNullLessThan = true) :
        base(comparisons, isNullLessThan)
    {
        this.getHashCode = getHashCode;
    }

    // interfaces

    #region IEqualityComparer Members

    public new bool Equals(object x, object y)
    {
        int comparison = Compare(x, y);
        bool isEqual = comparison == 0;
        return isEqual;
    }

    public int GetHashCode(object obj)
    {
        int hashCode = 0;
        T o = default(T);
        if (obj is T)
        {
            o = (T)(obj);
        }
        hashCode = getHashCode(o);
        return hashCode;
    }

    #endregion

    #region IEqualityComparer<T> Members

    public bool Equals(T x, T y)
    {
        int comparison = Compare(x, y);
        bool isEqual = comparison == 0;
        return isEqual;
    }

    public int GetHashCode(T obj)
    {
        int hashCode = getHashCode(obj);
        return hashCode;
    }

    #endregion

}



No real secret sauce here; merely account for GetHashCode and interpret an underlying comparison.

Thursday, February 18, 2010

Unit testing, better design

Ahem, so before we dig into some code, I would like to take a minute and say something.

I can be a very stubborn and obstinate person, and I am also incredibly lazy! As such, even having attended many lectures and throwing thousands and thousands of dollars at some of the finest academia, I ignored many of the simple, basic, fundamental principles of Computing they tried to instill in me.

Principles like
  1. Interface programming,
  2. Encapsulation,
  3. Separation of Concern [SoC],
I am positive there are more, but these three form a triad of sorts, themes in my most recent adventures.

This sad state of ignorance was further compounded by youth, inexperience, and entering the "real world", where constraints prohibited my ability to exercise these principles.

Many, many, many years later, after much trial, error, and tribulation I have entered something of a personal [and professional!] renaissance. Rediscovering some of these principles, I am continually finding new ways to simplify, extend, and robustify [alright, that is not really a word, but whatever] my work.

If that were not enough, then may I also add: Applying these simple yet powerful principles facilitates unit testing! Yes! That is right! Not only will you produce better work, but you will be able to quantitatively and qualitatively prove it is better.

If you are not yet sold, I do ask that you continue reading. Certainly, for the attentive reader, you will see these three principles come up time and time again in the [real world!] examples I present.

As I have said, it has taken me a long time to come around and see this for myself. It is my hope here and now, that you benefit from my experience: When designing a functional component, think about how Interfaces, Encapsulation, and SoC can help you. Invariably, such considerations will lead to better design and better product.