I created a base class to help me reduce boilerplate code of the initialization of the immutable Objects in C#,
I\'m using lazy initialization in order to try not to imp
As was already mentioned in the comments, it would make more sense, not to "conflate" the immutable instance implementation or interface with the behavior of what is essentially a builder for new instances.
You could make a much cleaner and quite type safe solution that way. So we could define some marker interfaces and type safe versions thereof:
public interface IImmutable : ICloneable { }
public interface IImmutableBuilder { }
public interface IImmutableOf : IImmutable where T : class, IImmutable
{
IImmutableBuilderFor Mutate();
}
public interface IImmutableBuilderFor : IImmutableBuilder where T : class, IImmutable
{
T Source { get; }
IImmutableBuilderFor Set(string fieldName, TFieldType value);
IImmutableBuilderFor Set(string fieldName, Func valueProvider);
IImmutableBuilderFor Set(Expression> fieldExpression, TFieldType value);
IImmutableBuilderFor Set(Expression> fieldExpression, Func valueProvider);
T Build();
}
And provide all the required basic builder behavior in a class like below. Note that most error checking/compiled delegate creation is omitted for the sake of brevity/simplicity. A cleaner, performance optimized version with a reasonable level of error checking can be found in this gist.
public class DefaultBuilderFor : IImmutableBuilderFor where T : class, IImmutableOf
{
private static readonly IDictionary>> _setters;
private List> _mutations = new List>();
static DefaultBuilderFor()
{
_setters = GetFieldSetters();
}
public DefaultBuilderFor(T instance)
{
Source = instance;
}
public T Source { get; private set; }
public IImmutableBuilderFor Set(string fieldName, TFieldType value)
{
// Notes: error checking omitted & add what to do if `TFieldType` is not "correct".
_mutations.Add(inst => _setters[fieldName].Item2(inst, value));
return this;
}
public IImmutableBuilderFor Set(string fieldName, Func valueProvider)
{
// Notes: error checking omitted & add what to do if `TFieldType` is not "correct".
_mutations.Add(inst => _setters[fieldName].Item2(inst, valueProvider(inst)));
return this;
}
public IImmutableBuilderFor Set(Expression> fieldExpression, TFieldType value)
{
// Error checking omitted.
var memberExpression = fieldExpression.Body as MemberExpression;
return Set(memberExpression.Member.Name, value);
}
public IImmutableBuilderFor Set(Expression> fieldExpression, Func valueProvider)
{
// Error checking omitted.
var memberExpression = fieldExpression.Body as MemberExpression;
var getter = fieldExpression.Compile();
return Set(memberExpression.Member.Name, inst => valueProvider(getter(inst)));
}
public T Build()
{
var result = (T)Source.Clone();
_mutations.ForEach(x => x(result));
return result;
}
private static IDictionary>> GetFieldSetters()
{
// Note: can be optimized using delegate setter creation (IL).
return typeof(T).GetFields(BindingFlags.Public | BindingFlags.Instance)
.Where(x => !x.IsLiteral)
.ToDictionary(
x => x.Name,
x => SetterEntry(x.FieldType, (inst, val) => x.SetValue(inst, val)));
}
private static Tuple> SetterEntry(Type type, Action setter)
{
return Tuple.Create(type, setter);
}
}
Example usage
This could then be used like this, using your example class of State
:
public static class Example
{
public class State : IImmutableOf
{
public State(int someInt, string someString)
{
SomeInt = someInt;
SomeString = someString;
}
public readonly int SomeInt;
public readonly string SomeString;
public IImmutableBuilderFor Mutate()
{
return new DefaultBuilderFor(this);
}
public object Clone()
{
return base.MemberwiseClone();
}
public override string ToString()
{
return string.Format("{0}, {1}", SomeInt, SomeString);
}
}
public static void Run()
{
var original = new State(10, "initial");
var mutatedInstance = original.Mutate()
.Set("SomeInt", 45)
.Set(x => x.SomeString, "Hello SO")
.Build();
Console.WriteLine(mutatedInstance);
mutatedInstance = original.Mutate()
.Set(x => x.SomeInt, val => val + 10)
.Build();
Console.WriteLine(mutatedInstance);
}
}
With the following output:
45, Hello SO
20, initial