5
\$\begingroup\$

I've come across a need for code like this a few times - you have an object (or objects) that may or may not have certain properties and values within those properties. Consider a program with multiple types that have a different set of "Name" type properties on each of them. Your Contact type has all of them, however, maybe the user type only has a first name, and middle initial.

I once had a project that had 5 different types with different assortments of those types of properties on them, and in each, had a method to get the Name, that looked through each and attempted to construct a string with all of the relevant information, and no leading, trailing, or double spaces. But this logic was duplicated.

At the time, I ended up just making single property interfaces for the different properties, IFirstName, ILastName, etc.. As well as an extension method off of a parent INamed interface that used whatever it had available to make the name. Applied this to the 5 types and their usages, and it definitely helped with reducing duplication of similar logic.

But I have always been hesitant on it, for what I feel is the lack of specificity/declarative-ness of the solution. Thus I have come up with the idea of a 'Fulfillment' pattern (I can't think of a better name, and am open to suggestions).

The idea is you have an interface for fulfillers:

public interface IFulfillable<TGive, TGet>
{
    bool Fulfillable(TGive give);

    TGet Fulfill(TGive give);
}

Now, I can make extension methods off of lists to find the closest match:

public static class IFulfillableExtensions
{
    public static TGet Fulfill<TGive, TGet>(this IEnumerable<IFulfillable<TGive,TGet>> fulfillers, TGive give)
    {   
        foreach (var fulfiller in fulfillers)
        {
            TGet get;
            if(fulfiller.TryFulfill(give, out get))
                return get;
        }

        throw new ArgumentOutOfRangeException("The given object is unable to be fulfilled by any of the specified fulfillers");
    }

    public static TGet Fulfill<TGive, TGet>(this IEnumerable<IFulfillable<TGive, TGet>> fulfillers, TGive give, TGet defaultGet)
    {
        foreach (var fulfiller in fulfillers)
        {
            TGet get;
            if (fulfiller.TryFulfill(give, out get))
                return get;
        }

        return defaultGet;
    }

    public static bool TryFulfill<TGive, TGet>(this IFulfillable<TGive, TGet> fulfillable, TGive give, out TGet get)
    {
        get = default(TGet);
        if (!fulfillable.Fulfillable(give))
            return false;

        get = fulfillable.Fulfill(give);

        return true;
    }
}

Expanding on my previous example of multiple objects with names, I can now use the following interfaces to set the ground work:

public interface IFirstName
{
    string FirstName { get;}
}

public interface ILastName
{
    string LastName { get;}
}

public interface IMiddleInitial
{
    char? MiddleInitial { get;}
}

public interface IMiddleName
{
    string MiddleName { get;}
}

public interface ISuffix
{
    string Suffix { get;}
}

public interface IPrefix
{
    string Prefix { get;}
}

And then using them:

public class Contact
    : IPrefix
    , IFirstName
    , IMiddleName
    , ILastName
    , ISuffix
{
    public string Prefix { get; set; }
    public string FirstName { get; set; }
    public string MiddleName { get; set; }
    public string LastName { get; set; }
    public string Suffix { get; set; }
}

public class User
    : IPrefix
    , IFirstName
    , IMiddleInitial
    , ILastName
{
    public string Prefix { get; set; }
    public string FirstName { get; set; }
    public char? MiddleInitial { get; set; }
    public string LastName { get; set; }
}

//<<Several other types that contain different combinations of 'name' interfaces>>

Now, I can define 'Fulfillers' or templates for the name, such as:

public class FullNameFulfill : IFulfillable<object, string>
{
    public static readonly FullNameFulfill Instance = new FullNameFulfill();

    private FullNameFulfill() { }

    public bool Fulfillable(object give)
    {
        return
            give is IPrefix && !String.IsNullOrWhiteSpace((give as IPrefix).Prefix)
         && give is IFirstName && !String.IsNullOrWhiteSpace((give as IFirstName).FirstName)
         && give is IMiddleName && !String.IsNullOrWhiteSpace((give as IMiddleName).MiddleName)
         && give is ILastName && !String.IsNullOrWhiteSpace((give as ILastName).LastName)
         && give is ISuffix && !String.IsNullOrWhiteSpace((give as ISuffix).Suffix);
    }

    public string Fulfill(object give)
    {
        var prefix = ((IPrefix)give).Prefix;
        var firstName = ((IFirstName)give).FirstName;
        var middleName = ((IMiddleName)give).MiddleName;
        var lastName = ((ILastName)give).LastName;
        var suffix = ((ISuffix)give).Suffix;

        if (String.IsNullOrWhiteSpace(prefix)
         || String.IsNullOrWhiteSpace(firstName)
         || String.IsNullOrWhiteSpace(middleName)
         || String.IsNullOrWhiteSpace(lastName)
         || String.IsNullOrWhiteSpace(suffix))
        {
            throw new ArgumentOutOfRangeException("Given object does not contain the necessary information to be fulfilled");
        }

        return $"{prefix} {firstName} {middleName} {lastName} {suffix}";
    }
}

public class FormalFirstnameFulfill : IFulfillable<object, string>
{
    public static readonly FormalFirstnameFulfill Instance = new FormalFirstnameFulfill();

    private FormalFirstnameFulfill() { }

    public bool Fulfillable(object give)
    {
        return
            give is IPrefix && !String.IsNullOrWhiteSpace((give as IPrefix).Prefix)
         && give is IFirstName && !String.IsNullOrWhiteSpace((give as IFirstName).FirstName);
    }

    public string Fulfill(object give)
    {
        var prefix = ((IPrefix)give).Prefix;
        var firstName = ((IFirstName)give).FirstName;

        if (String.IsNullOrWhiteSpace(prefix)
         || String.IsNullOrWhiteSpace(firstName))
        {
            throw new ArgumentOutOfRangeException("Given object does not contain the necessary information to be fulfilled");
        }

        return $"{prefix} {firstName}";
    }
}

public class FriendlyFirstNameWithMiddleNameFulfill : IFulfillable<object, string>
{
    public static readonly FriendlyFirstNameWithMiddleNameFulfill Instance = new FriendlyFirstNameWithMiddleNameFulfill();

    private FriendlyFirstNameWithMiddleNameFulfill() { }

    public bool Fulfillable(object give)
    {
        return
            give is IFirstName && !String.IsNullOrWhiteSpace((give as IFirstName).FirstName)
         && give is IMiddleName && !String.IsNullOrWhiteSpace((give as IMiddleName).MiddleName);
    }

    public string Fulfill(object give)
    {
        var firstName = ((IFirstName)give).FirstName;
        var middleName = ((IMiddleName)give).MiddleName;

        if (String.IsNullOrWhiteSpace(firstName)
         || String.IsNullOrWhiteSpace(middleName))
        {
            throw new ArgumentOutOfRangeException("Given object does not contain the necessary information to be fulfilled");
        }

        return $"{firstName} {middleName}";
    }
}

public class FriendlyFirstNameWithMiddleInitialFulfill : IFulfillable<object, string>
{
    public static readonly FriendlyFirstNameWithMiddleInitialFulfill Instance = new FriendlyFirstNameWithMiddleInitialFulfill();

    private FriendlyFirstNameWithMiddleInitialFulfill() { }

    public bool Fulfillable(object give)
    {
        return
            give is IFirstName && !String.IsNullOrWhiteSpace((give as IFirstName).FirstName)
         && give is IMiddleInitial && (give as IMiddleInitial).MiddleInitial.HasValue;
    }

    public string Fulfill(object give)
    {
        var firstName = ((IFirstName)give).FirstName;
        var middleInitial = ((IMiddleInitial)give).MiddleInitial;

        if (String.IsNullOrWhiteSpace(firstName)
         || middleInitial.HasValue)
        {
            throw new ArgumentOutOfRangeException("Given object does not contain the necessary information to be fulfilled");
        }

        return $"{firstName} {middleInitial}.";
    }
}

public class FriendlyFirstNameFulfill : IFulfillable<object, string>
{
    public static readonly FriendlyFirstNameFulfill Instance = new FriendlyFirstNameFulfill();

    private FriendlyFirstNameFulfill() { }

    public bool Fulfillable(object give)
    {
        return
            give is IFirstName && !String.IsNullOrWhiteSpace((give as IFirstName).FirstName);
    }

    public string Fulfill(object give)
    {
        var firstName = ((IFirstName)give).FirstName;

        if (String.IsNullOrWhiteSpace(firstName))
        {
            throw new ArgumentOutOfRangeException("Given object does not contain the necessary information to be fulfilled");
        }

        return $"{firstName}";
    }
}

public class NamedTypeUnknownNameFulfill : IFulfillable<object, string>
{
    public static readonly NamedTypeUnknownNameFulfill Instance = new NamedTypeUnknownNameFulfill();

    private NamedTypeUnknownNameFulfill() { }

    public bool Fulfillable(object give)
    {
        // only handle a 'name' type, don't return an "Unknown" name for any object
        return  give is IPrefix 
             || give is IFirstName 
             || give is IMiddleName 
             || give is IMiddleInitial
             || give is ILastName 
             || give is ISuffix;
    }

    public string Fulfill(object give)
    {
        var namedType = give is IPrefix
                     || give is IFirstName
                     || give is IMiddleName
                     || give is IMiddleInitial
                     || give is ILastName
                     || give is ISuffix;

        if (!namedType)
        {
            throw new ArgumentOutOfRangeException("Given object is not a named type");
        }

        return "Anonymous/Unknown";
    }
}

And finally, a class that can expose template/prebuilt lists in order of wanted use:

public static class NameFormatters
{
    public static List<IFulfillable<object, string>> AllFormatters = new List<IFulfillable<object, string>>
    {   
        FullNameFulfill.Instance
      , FormalFirstnameFulfill.Instance
      , FriendlyFirstNameWithMiddleNameFulfill.Instance
      , FriendlyFirstNameWithMiddleInitialFulfill.Instance
      , FriendlyFirstNameFulfill.Instance
      , NamedTypeUnknownNameFulfill.Instance
    };

    public static List<IFulfillable<object, string>> FirstNameFormatters = new List<IFulfillable<object, string>>
    {
        FriendlyFirstNameWithMiddleNameFulfill.Instance
      , FormalFirstnameFulfill.Instance
      , FriendlyFirstNameWithMiddleInitialFulfill.Instance
    };
}

Now for usage:

var fullContact = new Contact() { FirstName = "Raymond", MiddleName = "Red", LastName = "Reddington", Prefix = "Mr.", Suffix = "I" };
Console.WriteLine(NameFormatters.AllFormatters.Fulfill(fullContact));
//Outputs: Mr. Raymond Red Reddington I

fullContact.LastName = null;
Console.WriteLine(NameFormatters.AllFormatters.Fulfill(fullContact));
//Outputs: Mr. Raymond

fullContact.Prefix = null;
Console.WriteLine(NameFormatters.AllFormatters.Fulfill(fullContact));
//Outputs: Raymond Red

fullContact.MiddleName = null;
Console.WriteLine(NameFormatters.AllFormatters.Fulfill(fullContact));
//Outputs: Raymond

fullContact.FirstName = null;
Console.WriteLine(NameFormatters.AllFormatters.Fulfill(fullContact));
//Outputs: Anonymous/Unknown

As you can see, this allows for specifying templates, in what I feel is a more declarative way than just string building. It is also clear to see what the output of a 'fulfiller' would be. I can then also make a generic/simple fulfiller like this:

public class SimpleFulfillable<TGive, TGet>
    : IFulfillable<TGive, TGet>
{
    private readonly Func<TGive, bool> fulfillable;
    private readonly Func<TGive, TGet> fulfill;

    public bool Fulfillable(TGive give)
    {
        return fulfillable(give);
    }

    public TGet Fulfill(TGive give)
    {
        if (!Fulfillable(give))
            throw new ArgumentOutOfRangeException("The given object can not be fulfilled by this fulfiller");

        return fulfill(give);
    }

    public SimpleFulfillable(Func<TGive, bool> fulfillable, Func<TGive, TGet> fulfill)
    {
        this.fulfillable = fulfillable;
        this.fulfill = fulfill;
    }
}

A few questions:

  1. Is this a pattern that I can't think of? I keep feeling like it is some type of one, but can't place it. In my head, it feels somewhat like an inverse specification, but i think that's just me being crazy.
  2. Does this seem like it could be useful?
  3. Would YOU use it?
  4. If not - why not? Just want an idea of its lacking
  5. The thing that kills me is the name. I'm not a fan of IFulfillable, but I am not thinking of another currently.

I actually would change a few of the items in usage, like a name formatter base type, inheriting from IFulfillabe<object,string> to enforce usage in the static list classes, just scratch padding in linqpad to make this post. I'd also change several of the as casts above to explicit because they are done after checks so they wouldn't be necessary any longer. Not necessarily looking for feedback on that type of thing, but I'm open to it.

\$\endgroup\$
  • \$\begingroup\$ Thought i'd make one clarification, as I don't feel I like the title I put. The 'pattern-matching' that I speak of isn't in the source data. I feel that I am more matching what the source has to a pattern of a template - rearranging the input data to form some output pattern. But I feel this is also a lousy description. I apologize if this created any confusion. \$\endgroup\$ – tostringtheory Feb 5 '16 at 18:08
  • \$\begingroup\$ FYI - thanks @Jamal for editing the code to add the inline source. I completely forgot to go back through and add those in where needed. I do that on SO, just completely forgot here. \$\endgroup\$ – tostringtheory Feb 5 '16 at 18:14
  • \$\begingroup\$ Welcome to Code Review! I hope you get some helpful answers. \$\endgroup\$ – SirPython Feb 5 '16 at 18:50
3
\$\begingroup\$

Your current implementation is closed for extension a thus it violates the OCP (Open/Closed Priciple). You cannot extend it witout modifying the core FullNameFulfill class.

This task is another good exmaple for the usefulness of the composite pattern.

You first create an abstract class:

abstract class PropertyFormatter
{
    public static PropertyFormatter Empty = new CompositePropertyFormatter();

    public string FormatString { get; set; } = "{0}";
    public abstract string Format(object obj);
}

this will be the base for each property formatter.

Next you implement it for each interface (hier just two):

class FirstNameFormatter : PropertyFormatter
{
    public override string Format(object obj)
    {
        return (obj as IFirstName)?.FirstName;
    }
}

class LastNameFormatter : PropertyFormatter
{
    public override string Format(object obj)
    {
        return (obj as ILastName)?.LastName;
    }
}

Now you need to be able to combine them so you create:

class CompositePropertyFormatter : PropertyFormatter
{
    private readonly PropertyFormatter[] _formatters;

    public CompositePropertyFormatter(params PropertyFormatter[] formatters)
    {
        _formatters = formatters;
    }   

    public override string Format(object obj)
    {
        return string.Join(string.Empty, _formatters.Select(x 
            => string.Format(x.FormatString, x.Format(obj)))
        ).Trim();
    }
}

and a helper extension:

static class PropertyFormatterComposer
{
    public static CompositePropertyFormatter Add<T>(
        this PropertyFormatter formatter, 
        string formatString = " {0}") 
        where T : PropertyFormatter, new()
    {
        return new CompositePropertyFormatter(
            formatter, 
            new T { FormatString = formatString });
    }
}

Having all this you can now add a new formatter anytime you want without touching the core. Hier is how you use it:

var contact = new Contact
{
    FirstName = "Foo",
    LastName = "Bar"
};

var contactFormatter = PropertyFormatter.Empty
    .Add<FirstNameFormatter>()
    .Add<LastNameFormatter>(", {0}");

var result = contactFormatter.Format(contact);

result:

Foo, Bar
\$\endgroup\$

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.