Simplify constructors with builder pattern
In a previous blog post I created some helper classes and methods to make test code more reusable. One of those classes had a constructor with 3 parameters:
public SubmissionScenario(
WebApplicationUnderTest application,
TimeSpan phase1Delay,
TimeSpan phase2Delay
)
{
_application = application;
_httpClient = _application.Factory.CreateClient();
SetUpApiResponses(phase1Delay, phase2Delay);
}
With this approach, the number of its parameters could easily grow to 10 or more as the test scenarios became more complex and configurable. That many parameters will make any constructor or method call difficult to understand. The builder design pattern is very applicable in such cases, especially if parameters are optional and don't always need to be set.
The following builder implementation takes advantage of the existing constructor:
public class SubmissionScenarioBuilder(WebApplicationUnderTest application)
{
private TimeSpan _phase1Delay = TimeSpan.FromMilliseconds(1);
private TimeSpan _phase2Delay = TimeSpan.FromMilliseconds(1);
public SubmissionScenarioBuilder WithPhase1Delay(TimeSpan delay)
{
_phase1Delay = delay;
return this;
}
public SubmissionScenarioBuilder WithPhase2Delay(TimeSpan delay)
{
_phase2Delay = delay;
return this;
}
public SubmissionScenario Build()
{
return new SubmissionScenario(application, _phase1Delay, _phase2Delay);
}
}
Its constructor accepts only the required parameter. The other two parameters can be set using the fluent syntax:
var scenario = new SubmissionScenarioBuilder(_application)
.WithPhase1Delay(TimeSpan.FromSeconds(0.6))
.WithPhase2Delay(TimeSpan.FromSeconds(0.6))
.Build();
With the introduction of init-only setters in C# 9, the methods in the builder class could be replaced with such properties:
public class SubmissionScenarioBuilder(WebApplicationUnderTest application)
{
public TimeSpan Phase1Delay { private get; init; } =
TimeSpan.FromMilliseconds(1);
public TimeSpan Phase2Delay { private get; init; } =
TimeSpan.FromMilliseconds(1);
public SubmissionScenario Build()
{
return new SubmissionScenario(application, Phase1Delay, Phase2Delay);
}
}
The builder class could then be initialized with the object initializer instead of the fluent syntax:
var scenario = new SubmissionScenarioBuilder(_application)
{
Phase1Delay = TimeSpan.FromSeconds(0.6),
Phase2Delay = TimeSpan.FromSeconds(0.6),
}.Build();
The nice thing about the init-only setters is that the resulting properties are still immutable, i.e., they cannot be changed after the object initialization. So, why not implement the properties directly in the scenario class, skipping the need for the builder altogether? This would make the scenario initialization even simpler without the builder intermediary:
var scenario = new SubmissionScenario(_application)
{
Phase1Delay = TimeSpan.FromSeconds(0.6),
Phase2Delay = TimeSpan.FromSeconds(0.6),
};
It could be done, but it's important to keep in mind that the constructor code runs before properties are set by the object initializer. Therefore, any code in the constructor that depends on the property values must be run (again) when a property value changes. Thanks to the field keyword that was introduced in C# 14, this can be achieved without adding separate backing fields:
public SubmissionScenario(WebApplicationUnderTest application)
{
_application = application;
_httpClient = _application.Factory.CreateClient();
SetUpApiResponses(Phase1Delay, Phase2Delay);
}
public TimeSpan Phase1Delay
{
private get;
init
{
field = value;
SetUpApiResponses(Phase1Delay, Phase2Delay);
}
} = TimeSpan.FromMilliseconds(1);
public TimeSpan Phase2Delay
{
private get;
init
{
field = value;
SetUpApiResponses(Phase1Delay, Phase2Delay);
}
} = TimeSpan.FromMilliseconds(1);
As the number of properties increases, this can still quickly become unwieldy. Keeping a separate builder class is therefore a good idea, even with all the recent C# features at your disposal.
You can find a full sample project in my GitHub repository. Each approach is implemented in its own commit, fully used across all tests. You can easily compare their pros and cons on your own.
The builder pattern is a great choice whenever your constructors have too many parameters, but you still want to keep the resulting object immutable after initialization. In this blog post I presented two different implementations of the builder pattern in C#, along with an alternative without a builder for comparison.
