Last active
December 16, 2015 06:59
-
-
Save bradphelan/5395652 to your computer and use it in GitHub Desktop.
Immutable persistent object pattern in C#. We want to be able to set a property at an arbitrary depth and return a new tree without changing the old tree. This is possible with a few reflection tricks. I have a very specific use case for this and it is to build an undo stack for my application without a messy command pattern system.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Fact] | |
public void UndoStackChangeNotificationSpec() | |
{ | |
var stack = new UndoStack<A>(new A(10, null)); | |
var psubj = stack.ImmutablePropertySubject(x => x.P); | |
int current = 0; | |
psubj.Subscribe(c => current = c); | |
psubj.OnNext(20); | |
stack.Current.P.Should().Be(20); | |
current.Should().Be(20); | |
psubj.OnNext(30); | |
stack.Current.P.Should().Be(30); | |
current.Should().Be(30); | |
psubj.OnNext(40); | |
stack.Current.P.Should().Be(40); | |
current.Should().Be(40); | |
stack.Undo(); | |
stack.Current.P.Should().Be(30); | |
current.Should().Be(30); | |
stack.Undo(); | |
stack.Current.P.Should().Be(20); | |
current.Should().Be(20); | |
stack.Undo(); | |
stack.Current.P.Should().Be(10); | |
current.Should().Be(10); | |
// Verify we don't pop the initial value | |
stack.Undo(); | |
stack.Current.Should().NotBe(null); | |
current.Should().Be(10); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
interface Immutable { }; | |
static class ImmutableExtension | |
{ | |
public static T Set<T,V> | |
(this T This, Expression<Func<T, V>> prop, V value) | |
where T : class, Immutable | |
{ | |
var names = ReactiveUI.Reflection.ExpressionToPropertyNames(prop).ToList(); | |
return This.Set(names, value); | |
} | |
public static T Set<T,V> | |
(this T This, List<string> names, V value) | |
where T : class, Immutable | |
{ | |
var name = names.First(); | |
var rest = names.Skip(1).ToList(); | |
if (names.Count==1) | |
{ | |
var copy = This.ShallowClone(); | |
copy.SetPrivatePropertyValue(names.First(), value); | |
return copy as T; | |
}else{ | |
var copy = This.ShallowClone(); | |
var subtree = copy | |
.GetPrivatePropertyValue<Immutable>(name) | |
.Set(rest, value); | |
copy.SetPrivatePropertyValue(names.First(), subtree ); | |
return copy as T; | |
} | |
} | |
public static object ShallowClone(this object o) | |
{ | |
var flags = BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance; | |
return o.GetType().InvokeMember("MemberwiseClone", flags, null, o, null); | |
} | |
/// <summary> | |
/// Sets a _private_ Property Value from a given Object. Uses Reflection. | |
/// Throws a ArgumentOutOfRangeException if the Property is not found. | |
/// </summary> | |
/// <typeparam name="T">Type of the Property</typeparam> | |
/// <param name="obj">Object from where the Property Value is set</param> | |
/// <param name="propName">Propertyname as string.</param> | |
/// <param name="val">Value to set.</param> | |
/// <returns>PropertyValue</returns> | |
public static void SetPrivatePropertyValue<T>(this object obj, string propName, T val) | |
{ | |
Type t = obj.GetType(); | |
if (t.GetProperty(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) == null) | |
throw new ArgumentOutOfRangeException("propName", string.Format("Property {0} was not found in Type {1}", propName, obj.GetType().FullName)); | |
t.InvokeMember(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.SetProperty | BindingFlags.Instance, null, obj, new object[] { val }); | |
} | |
/// <summary> | |
/// Returns a _private_ Property Value from a given Object. Uses Reflection. | |
/// Throws a ArgumentOutOfRangeException if the Property is not found. | |
/// </summary> | |
/// <typeparam name="T">Type of the Property</typeparam> | |
/// <param name="obj">Object from where the Property Value is returned</param> | |
/// <param name="propName">Propertyname as string.</param> | |
/// <returns>PropertyValue</returns> | |
public static T GetPrivatePropertyValue<T>(this object obj, string propName) | |
{ | |
if (obj == null) throw new ArgumentNullException("obj"); | |
PropertyInfo pi = obj.GetType().GetProperty(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); | |
if (pi == null) throw new ArgumentOutOfRangeException("propName", string.Format("Property {0} was not found in Type {1}", propName, obj.GetType().FullName)); | |
return (T)pi.GetValue(obj, null); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class A : Immutable | |
{ | |
public int P { get; private set; } | |
public B B { get; private set; } | |
public A(int p, B b) | |
{ | |
P = p; | |
B = b; | |
} | |
} | |
class B : Immutable | |
{ | |
public int P { get; private set; } | |
public C C { get; private set; } | |
public B(int p, C c) | |
{ | |
P = p; | |
C = c; | |
} | |
} | |
class C : Immutable | |
{ | |
public int P { get; private set; } | |
public C(int p) | |
{ | |
P = p; | |
} | |
} | |
namespace Utils.Spec | |
{ | |
public class ImmutableObjectPatternSpec | |
{ | |
[Fact] | |
public void OhMyLord() | |
{ | |
var a = new A | |
( p:10 | |
, b: new B | |
( p: 20 | |
, c : new C(30))); | |
var a_ = a.Set(p => p.B.C.P, 10); | |
a.Should().NotBe(a_); | |
a.B.C.P.Should().Be(30); | |
a_.B.C.P.Should().Be(10); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Fact] | |
public void UndoStackLensSpec() | |
{ | |
var stack0 = new UndoStack<A>(new A(1, new B(1,new C(5)))); | |
// Create a lens onto A.B a lens is ISubject<T> but | |
// also remembers the property path it was generated | |
// from | |
var bLens = stack0.ImmutablePropertySubject(x => x.B); | |
// Create a sub lens to A.B.C.P | |
var bp = bLens.ForProperty(x => x.C.P); | |
// Change the property | |
bp.OnNext(10); | |
// Verify that it is changed in the undo stack | |
stack0.Current.B.C.P.Should().Be(10); | |
stack0.Undo(); | |
// Verify that undo still works | |
stack0.Current.B.C.P.Should().Be(5); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class UndoStack<T> | |
where T : class, Immutable | |
{ | |
private Stack<T> ChangeHistory = new Stack<T>(); | |
public T Set<V> | |
(Expression<Func<T, V>> prop, V value) | |
{ | |
var last = ChangeHistory.Peek(); | |
var next = last.Set(prop, value); | |
ChangeHistory.Push(next); | |
return next; | |
} | |
public T Undo() | |
{ | |
ChangeHistory.Pop(); | |
return ChangeHistory.Peek(); | |
} | |
public UndoStack(T initial) | |
{ | |
ChangeHistory.Push(initial); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Fact] | |
public void UndoStackSpec() | |
{ | |
var stack = new UndoStack<A>(new A(10, null)); | |
stack.Current().B.Should().Be(null); | |
stack.Set(x => x.B, new B(20, null)); | |
stack.Current().B.Should().NotBe(null); | |
stack.Current().B.P.Should().Be(20); | |
stack.Undo(); | |
stack.Current().B.Should().Be(null); | |
} |
You should read through the old ReactiveUI.Serialization code, it did something similar to implement object persistence a-la Google Wave, where you could traverse object "history"
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The sort of typical cut and paste errors that this kind of code generates are things like this.
Note the Id on the right hand side of the Value assignment. Easy
to do cause you copy the above line and forget to change the
name in both places. If the types are the same then you get
no compiler error and you then have to write tests for this
behaviour which will probably generate copy paste errors.