I have a Shift
class that looks like this:
public class Shift
{
public string Name { get; private set; }
public TimeSpan StartHour { get; private set; }
public TimeSpan EndHour { get; private set; }
public TimeSpan Duration => EndHour - StartHour;
private Shift(string name, TimeSpan startHour, TimeSpan endHour)
{
Name = name;
StartHour = startHour;
EndHour = endHour;
}
private Shift()
{
}
}
Since there are some limitations and restrictions to what StartHour
&& EndHour
can be I needed some way to validate the input of the class and if it fails - prevent the creation of the object or at least not return it to the user, without throwing an exception because I want to allow the user to re-enter the arguments if some of them were invalid.
Than I created an interface and some derived classes to be injected during the construction of the object:
public interface IShiftValidator
{
Exception FailedValidationException { get; }
string FailedValidationMessage { get; }
bool IsValid(Shift shiftToValidate);
}
public class ShiftRangeValidator : IShiftValidator
{
public Exception FailedValidationException { get; private set; }
public string FailedValidationMessage { get; private set; } = string.Empty;
public bool IsValid(Shift shiftToValidate)
{
if (shiftToValidate.Duration.TotalMinutes <= 0)
{
FailedValidationException = new ArgumentOutOfRangeException(nameof(shiftToValidate.Duration));
FailedValidationMessage = "Shift's duration cant be less than 1 minute.";
return false;
}
if (shiftToValidate.Duration.TotalHours > SharedSettings.MaximumShiftHours)
{
FailedValidationException =
new ArgumentOutOfRangeException(nameof(shiftToValidate.Duration));
FailedValidationMessage = "Shift's duration cant be longer than the maximum allowed hours.";
return false;
}
return true;
}
}
After that I implemented the builder pattern as a nested class in the Shift
class, so I can make the constructor of the class private and construct the object fully in the builder class:
public class ShiftBuilder
{
private Shift _shift;
public ShiftBuilder()
{
_shift = new Shift();
}
public ShiftBuilder WithName(string name)
{
_shift.Name = name;
return this;
}
public ShiftBuilder WithStartHour(TimeSpan startHour)
{
_shift.StartHour = startHour;
return this;
}
public ShiftBuilder WithEndHour(TimeSpan endHour)
{
_shift.EndHour = endHour;
return this;
}
public Shift Build(IEnumerable<IShiftValidator> validators)
{
if (validators != null)
{
foreach (var shiftValidator in validators)
{
if (!shiftValidator.IsValid(_shift))
{
if (!string.IsNullOrEmpty(shiftValidator.FailedValidationMessage))
{
MessageBox.Show(shiftValidator.FailedValidationMessage);
}
throw shiftValidator.FailedValidationException;
}
}
}
Shift shift = _shift;
_shift = null;
return shift;
}
}
Here's an example usage:
Shift.ShiftBuilder shiftBuilder = new Shift.ShiftBuilder();
var a = shiftBuilder.WithName("Test")
.WithStartHour(new TimeSpan(12, 0, 0))
.WithEndHour(new TimeSpan(24, 0, 0))
.Build(new[] { new ShiftRangeValidator() });
var b = shiftBuilder.Build(null); // null
var c = shiftBuilder.Build(new[] {new ShiftRangeValidator()}); // crash
I have few concerns:
- Is it clear that you're unable to reuse the same
ShiftBuilder
twice?
- Is
IShiftValidator
good as it is or should I separate it in few smaller interfaces?