I'm trying to compose a builder pattern which is extended by implementing interfaces multiple times.
Here is a code snippet for what I'm trying to do.
namespace BuilderChaining
{
internal interface IBuilder<A, B>
{
IBuilder<A, B> BuilderCapability();
}
internal interface IConsumeOtherBuilder<A, B, C>
{
IConsumeOtherBuilder<A, B, C> AdditionalBuilderFeature(IBuilder<B, C> obj);
}
internal class ExampleBuilder : IBuilder<int, string>, IConsumeOtherBuilder<int, string, object>, IConsumeOtherBuilder<Guid, object, int>
{
public IConsumeOtherBuilder<int, string, object> AdditionalBuilderFeature(IBuilder<string, object> obj)
{
return this;
}
public IConsumeOtherBuilder<Guid, object, int> AdditionalBuilderFeature(IBuilder<object, int> obj)
{
return this;
}
public IBuilder<int, string> BuilderCapability()
{
return this;
}
}
internal class Program
{
static void Main(string[] args)
{
var builder = new ExampleBuilder();
builder.BuilderCapability() //Ideally the call to .AdditionalBuilderFeature() would be allowed here. At the moment, it doesn't compile however...
.AdditionalBuilderFeature(/*Some param value*/)
.AdditionalBuilderFeature(/*Some param value*/);
}
}
}
How do I create builder patterns that can be extended by interfaces yet still manage to return the main builder class in order to preserve the capability of calling all methods implemented for a builder?
One thing that I tried before creating a minimal example was to take in a "Self" type parameter at the class level. That fell apart due to builders sometimes needing to take in other builders as method parameters, and the complexities of having the "Self" type be correct when there may be multiple implementations.
Taking some ideas from Builder design pattern with inheritance: is there a better way? I've managed to come up with this as a solution. However, it loses a lot in ergonomics, as you need to tell it at each step the correct type to return. It also leads the compiler to emit possible null reference return warnings.
internal interface IBuilder<A, B>
{
Self BuilderCapability<Self>()
where Self : class, IBuilder<A, B>;
}
internal interface IConsumeOtherBuilder<A, B, C>
{
Self AdditionalBuilderFeature<Self>(IBuilder<B, C> obj)
where Self : class, IConsumeOtherBuilder<A, B, C>;
}
internal class ExampleBuilder : IBuilder<int, string>, IConsumeOtherBuilder<int, string, object>, IConsumeOtherBuilder<Guid, object, int>
{
public Self AdditionalBuilderFeature<Self>(IBuilder<string, object> obj)
where Self : class, IConsumeOtherBuilder<int, string, object>
{
Console.WriteLine($"{nameof(ExampleBuilder.AdditionalBuilderFeature)} with self type: {typeof(Self).FullName}");
obj.BuilderCapability<IBuilder<string, object>>();
return this as Self;
}
public Self AdditionalBuilderFeature<Self>(IBuilder<object, int> obj)
where Self : class, IConsumeOtherBuilder<Guid, object, int>
{
Console.WriteLine($"{nameof(ExampleBuilder.AdditionalBuilderFeature)} with self type: {typeof(Self).FullName}");
obj.BuilderCapability<IBuilder<object, int>>();
return this as Self;
}
public Self BuilderCapability<Self>()
where Self : class, IBuilder<int, string>
{
Console.WriteLine($"{nameof(ExampleBuilder.BuilderCapability)} with self type: {typeof(Self).FullName}");
return this as Self;
}
}
internal class OtherBuilders<A, B> : IBuilder<A, B>
{
public Self BuilderCapability<Self>()
where Self : class, IBuilder<A, B>
{
Console.WriteLine($"{nameof(OtherBuilders<A, B>.BuilderCapability)} with self type: {typeof(Self).FullName}");
return this as Self;
}
}
internal class Program
{
static void Main(string[] args)
{
var builder = new ExampleBuilder();
builder.BuilderCapability<ExampleBuilder>()
.AdditionalBuilderFeature<ExampleBuilder>(new OtherBuilders<string, object>())
.AdditionalBuilderFeature<ExampleBuilder>(new OtherBuilders<object, int>());
}
}
One last note: A version of this pattern is currently used for building data in tests primarily. Solutions that might not be appropriate for production code might be acceptable here.
While not a workaround for covariant return types in interfaces, the following solution works for my problem. Your millage my vary!
namespace BuilderChaining
{
internal interface IBuilder<A, B>
{
IBuilder<A, B> BuilderCapability();
}
internal interface IBuilderSelf<A, B, Self> : IBuilder<A, B>
{
new Self BuilderCapability();
}
internal interface IConsumeOtherBuilder<A, B, C>
{
IConsumeOtherBuilder<A, B, C> AdditionalBuilderFeature(IBuilder<B, C> obj);
}
internal interface IConsumeOtherBuilderSelf<A, B, C, Self> : IConsumeOtherBuilder<A, B, C>
{
new Self AdditionalBuilderFeature(IBuilder<B, C> obj);
}
internal class ExampleBuilder : IBuilderSelf<int, string, ExampleBuilder>, IConsumeOtherBuilderSelf<int, string, object, ExampleBuilder>, IConsumeOtherBuilderSelf<Guid, object, int, ExampleBuilder>
{
public ExampleBuilder AdditionalBuilderFeature(IBuilder<string, object> obj)
{
obj.BuilderCapability();
return this;
}
public ExampleBuilder AdditionalBuilderFeature(IBuilder<object, int> obj)
{
obj.BuilderCapability();
return this;
}
public ExampleBuilder BuilderCapability()
{
return this;
}
IConsumeOtherBuilder<int, string, object> IConsumeOtherBuilder<int, string, object>.AdditionalBuilderFeature(IBuilder<string, object> obj)
{
return AdditionalBuilderFeature(obj);
}
IConsumeOtherBuilder<Guid, object, int> IConsumeOtherBuilder<Guid, object, int>.AdditionalBuilderFeature(IBuilder<object, int> obj)
{
return AdditionalBuilderFeature(obj);
}
IBuilder<int, string> IBuilder<int, string>.BuilderCapability()
{
return BuilderCapability();
}
}
internal class OtherBuilders<A, B> : IBuilderSelf<A, B, OtherBuilders<A, B>>
{
public OtherBuilders<A, B> BuilderCapability()
{
return this;
}
IBuilder<A, B> IBuilder<A, B>.BuilderCapability()
{
return BuilderCapability();
}
}
internal class Program
{
static void Main()
{
var builder = new ExampleBuilder();
builder.BuilderCapability()
.AdditionalBuilderFeature(new OtherBuilders<string, object>())
.AdditionalBuilderFeature(new OtherBuilders<object, int>());
}
}
}