views:

44

answers:

2

I have two extension methods that are very similar. I'd like to remove the code duplicates with «the hole in the middle» pattern or the like, but can't really get it to work.

The code looks like so:

public static String GetPublicPropertiesAsString( this Object @this )
{
    return @this.GetType().GetProperties()
        .Select( propertyInfo =>
                        {
                            var propertyValue = propertyInfo.GetValue( obj: @this,
                                                                    invokeAttr: BindingFlags.Public,
                                                                    binder: null,
                                                                    index: null,
                                                                    culture: null );

                            var propertyValueAsString = propertyValue != null ? propertyValue.ToString().RemoveAll( "00:00:00" ) : "[null]";

                            return "{0}: {1}".FormatWith( propertyInfo.Name, propertyValueAsString );
                        } ).JoinAsString( Environment.NewLine );
}

public static String GetFieldsAsString( this Object @this )
{
    return @this.GetType().GetFields()
        .Select( fieldInfo =>
                        {
                            var fieldValue = fieldInfo.GetValue( @this );

                            var fieldValueAsString = fieldValue != null ? fieldValue.ToString().RemoveAll( "00:00:00" ) : "[null]";

                            return "{0}: {1}".FormatWith( fieldInfo.Name, fieldValueAsString );
                        } ).JoinAsString( Environment.NewLine );
}

Could the repetitive code above be refactored away?

Note: JoinAsString, RemoveAll, and FormatWith are my own extension methods.

A: 

I would refactor that middle code out to a single method that accepts MemberInfo[], and does a switch on the member type. Untested, but:

    private static string GetMembersAsString(object @this, MemberInfo[] members)
    {
        var sb = new StringBuilder();
        for (int i = 0; i < members.Length; i++)
        {
            var member = members[i];
            if (i != 0) sb.AppendLine();
            object value;
            switch(member.MemberType) {
                case MemberTypes.Field:
                    value = ((FieldInfo)member).GetValue(@this);
                    break;
                case MemberTypes.Property:
                    value = ((PropertyInfo)member).GetValue(@this, null);
                    break;
                default:
                    throw new NotSupportedException(member.MemberType.ToString());
            }
            string s = value == null ? "[null]" : value.ToString().RemoveAll("00:00:00");
            sb.Append(member.Name).Append(" = ").Append(s);
        }
        return sb.ToString();
    }
    public static String GetPublicPropertiesAsString(this Object @this)
    {
        return GetMembersAsString(@this, @this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance));
    }

    public static String GetFieldsAsString(this Object @this)
    {
        return GetMembersAsString(@this, @this.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance));
    }
Marc Gravell
It might remove the code dupes, but it also refactors the original declarative code into imperative, which IMHO reduces readability.
Martin R-L
@Martin well, we could have retained FP by passing in the `GetValue` terms for each, but to be honest (and particularly on private implementation methods) I'm not sure that getting stressed over FP vs others is a debate worth having. As long as it works, etc....
Marc Gravell
+3  A: 

Yes, it can. By using generics and the fact that both PropertyInfo and FieldInfo derives from MemberInfo.

Something like this:

static string GetAsString<T>
    (object @this, Func<T, object> getter) where T : MemberInfo
{
  return @this.GetType().GetMembers(/* binding flags */).OfType<T>().Select(
    x => 
    {
      var value = getter(x);
      var valueAsString = value != null ? value.ToString().
           RemoveAll( "00:00:00" ) : "[null]";
      return "{0}: {1}".FormatWith( x.Name, valueAsString );
    }).JoinAsString();
}

Now the hook up:

public static String GetPublicPropertiesAsString( this Object @this )
{
  return GetAsString<PropertyInfo>(@this, x => x.GetValue(@this, null));
}

public static String GetPublicFieldsAsString( this Object @this )
{
  return GetAsString<FieldInfo>(@this, x => x.GetValue(@this));
}
leppie
+1 and accepted. Thanx!
Martin R-L