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:
- 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.
- Does this seem like it could be useful?
- Would YOU use it?
- If not - why not? Just want an idea of its lacking
- 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.