Automatic generation of immutable class and matching builder class
Asked Answered
E

2

13

What tools/libraries exist that will take a struct and automatically generate an immutable wrapper and also a "builder" class for incrementally building new instances?

Example input:

struct Foo
{
    public int apples;
    public int oranges;
    public Foo Clone() {return (Foo) base.MemberwiseClone();}
}

Example output:

public class ImmutableFoo // could probably be a struct
{
    private Foo snapshot;
    internal ImmutableFoo(Foo value) { this.snapshot = value; }
    public FooBuilder Builder() { return new FooBuilder(snapshot); }
    public int Apples { get { return snapshot.apples; } }
    public int Oranges { get { return snapshot.oranges; } }
}

public class FooBuilder
{
    private Foo state;

    public int Apples { get { return state.apples; } set { state.apples = value; } }
    public int Oranges { get { return state.oranges; } set { state.oranges = value; } }

    public FooBuilder() { }

    internal FooBuilder(Foo toCopy) { state = toCopy.Clone(); }

    public ImmutableFoo Build()
    {
        ImmutableFoo result = new ImmutableFoo(state);
        state = state.Clone();
        return result;
    }
}

Such a "tool" could be an IDE plugin or could generate the new class at run-time using reflection.

The example is in C# but I would be interested in a solution for any statically-typed OO language (Java, Scala, C++ etc.)

Desirable features:

  • Re-creates methods from the struct in the builder class
  • Re-creates nondestructive methods from the struct in the immutable class (esp. Equals() and GetHashCode() and any interface methods)
  • Also generates a IFooReader interface containing read-only properties for each struct member, implemented by both the immutable and the builder.
  • If a field's class has an immutable equivalent, uses the immutable version in the immutable class (see also How do I create a builder in C# for an object that has properties that are referenc types?) e.g. List -> ReadOnlyCollection or similar.
  • Alternatively take the builder class as input (where the builder uses automatic properties instead of delegating to a struct.)
  • Does not require the Clone method to be predefined

"You should not use a tool like this because..." answers are also welcome.

Eldreda answered 9/2, 2010 at 18:47 Comment(0)
K
4

Here are four possible solutions.

1) Use CodeDOM to generate C# or VB code. This would also allow you to use visual studio extensions to generate your code in designer files. Similar to some of the built in tools that visual studio already offers - like the ones that generate wrappers for web service calls etc. Unfortunately I don't know much about extending Visual Studio.

  • Pros - You can generate source prior to building. This makes it easier to write code against the generated types from any assembly.
  • Cons - Not language agnostic. You're stuck with the languages that are supported.

2) Use the Mono.Cecil library to analyze your assembly post-build. You can then re-write the assembly with the new types included.

  • Pros - Language agnostic.
  • Cons - If you add the types to same assembly in which your structs are defined you won't be able to write code against the generated immutable struct types in the same assembly. If you put the generated types in a new assembly then this is possible.

3) Use PostSharp. I don't know as much about this library so you might not be able to add new types to your assembly but I know you can inject IL into methods. It also has a lot of nice stuff that makes it easy to do this with attributes. So you could do this -

[GenerateImmutable]
struct Foo
{
    public int apples;
    public int oranges;
    public Foo Clone() {return (Foo) base.MemberwiseClone();}
}
  • Pros - Language agnostic, AOP is easier to do in PostSharp.
  • Cons - Same as with Mono.Cecil and also not sure if you can generate new types using PostSharp.

4) Use built in Reflection.Emit libraries to generate a new assembly with your immutable types.

  • Pros - Language agnostic, No 3rd party stuff.
  • Cons - Must put generated types in new assemblies. Can't add them to the same assembly that the original type is in.
Kimberykimble answered 12/2, 2010 at 2:22 Comment(1)
5) Use Roslyn to analyze your code, and t4 to generate builderFeudatory
A
1

Why bother with the builder?

You have a (nasty) mutable struct, but if you must have it use that directly rather than creating a cumbersome and unnecessary Builder.

It bothers me somewhat that you have sufficient number of these structs for you to feel you need to autogenerate wrappers of this kind. My gut reaction is that you are doing it wrong...

If the purpose of the immutable wrapper is just to store a snapshot then just use something like this:

public struct Snapshot<T> where t : struct
{
    private readonly T data;
    public Snapshot(T value) { this.data = value; }
    public T Data { get { return data; } }
}

The struct passed in is guaranteed to never change again, but you can access all the values on it directly (and modifications on these results happen on the copy created when calling the underlying get_Data function)

Addend answered 13/2, 2010 at 18:0 Comment(3)
The struct will be internal, but the immutable class and the builder class will be public. Your technique will work, but the ideal solution would have an equivalent in Java (where there are no value types, so the memberwise clone is probably unavoidable.)Eldreda
@Eldreda Why make the struct internal? I think you need to 'go up a level' and explain how/why this idiom would be used so we can better understand it. If you are designing things to be 'snapshotable' then just make them imutable to begin with make your api's of the form foo.WithX(x) -> foo where the change has happened. then any reference to a specific foo instance is itself a perfectly valid snapshot.Addend
@Eldreda I would actually say that, if you are bothering to auto generate so you can be similar in java why not just autogenerate a decent immutable implementation where every property/getFoo is readonly but a WithhFoo(x value) which returns a new instance of the object with that value changed is generated. Any instance of the object/struct is immutable, you have a nice fluent interface to alter things (much like some functional programming languages so reasonablly standardized). No need for builders or immutable versions, job done.Addend

© 2022 - 2024 — McMap. All rights reserved.