HiveBrain v1.2.0
Get Started
← Back to all entries
patterncsharpMinor

Pattern-matching-esque code

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
codeesquepatternmatching

Problem

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
{
    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(this IEnumerable> 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");
}

publ

Solution

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(
        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()
    .Add(", {0}");

var result = contactFormatter.Format(contact);


result:

Foo, Bar

Code Snippets

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

    public string FormatString { get; set; } = "{0}";
    public abstract string Format(object obj);
}
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;
    }
}
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();
    }
}
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 });
    }
}
var contact = new Contact
{
    FirstName = "Foo",
    LastName = "Bar"
};

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

var result = contactFormatter.Format(contact);

Context

StackExchange Code Review Q#119026, answer score: 3

Revisions (0)

No revisions yet.