Home > Back-end >  Fluent Builder Interface lets me set middle name and last name more than once
Fluent Builder Interface lets me set middle name and last name more than once

Time:10-13

I'm implementing a Fluent Builder Interface and instead of having null, empty or whitespace checks, I force the developer to fill in the following mandatory fields: firstName, prefix and facultyNumber. The other two fields middleName and lastName are optional.

When you try the snippet below, it will force you to fill in the fields. The issue is when it comes to IPrefixSelectionStage, it lets you add .WithMiddleName twice. Is there a way I can prevent that?

var asd = FluentAccountBuilder
    .Create()
    .WithFirstName("Иван")
    .WithMiddleName("Симеонов")
    .WithMiddleName("Asd") // shouldn't let me do that, even tho it's optional
    .WithLastName("Петков")
    .WithPrefix("cs")
    .WithFacultyNumber("196300")
    .Build();

Snippet

public record Account(
    string FirstName,
    string? MiddleName,
    string? LastName,
    string Username,
    string Password);

public sealed class FluentAccountBuilder :
    IFirstNameSelectionStage,
    IPrefixSelectionStage,
    IFacultyNumberSelectionStage,
    IBuildInitializerStage
{
    private static readonly Regex FacultyNumberRegex = new("^[0-9]{6}$", RegexOptions.Compiled);

    private string _firstName = string.Empty;
    private string? _middleName;
    private string? _lastName;
    private string _prefix = string.Empty;
    private string _facultyNumber = string.Empty;

    private FluentAccountBuilder()
    {
    }

    public static IFirstNameSelectionStage Create()
    {
        return new FluentAccountBuilder();
    }

    public IPrefixSelectionStage WithFirstName(string firstName)
    {
        if (string.IsNullOrWhiteSpace(firstName))
        {
            throw new ArgumentOutOfRangeException(nameof(firstName), "Please provide a valid FirstName");
        }

        _firstName = firstName;
        return this;
    }

    public IPrefixSelectionStage WithMiddleName(string middleName)
    {
        _middleName = middleName;
        return this;
    }

    public IPrefixSelectionStage WithLastName(string lastName)
    {
        _lastName = lastName;
        return this;
    }

    public IFacultyNumberSelectionStage WithPrefix(string prefix)
    {
        _prefix = prefix;
        return this;
    }

    public IBuildInitializerStage WithFacultyNumber(string facultyNumber)
    {
        if (!FacultyNumberRegex.IsMatch(facultyNumber))
        {
            throw new ArgumentOutOfRangeException(nameof(facultyNumber), "Faculty number must be 6-digits long");
        }

        _facultyNumber = facultyNumber;
        return this;
    }

    public Account Build()
    {
        // Transliterate name
        var transliteratedFirstName = Transliteration.CyrillicToLatin(_firstName);
        var transliteratedMiddleName = Transliteration.CyrillicToLatin(_middleName ?? string.Empty);
        var transliteratedLastName = Transliteration.CyrillicToLatin(_lastName ?? string.Empty);

        // Generate username and password
        // [cs/se][year-of-study][name-initials]
        var yearOfStudy = _facultyNumber[..2];

        var firstLetterOfFirstName = transliteratedFirstName.ToLowerInvariant()[..1];

        var firstLetterOfMiddleName = transliteratedMiddleName.Length > 0
            ? transliteratedMiddleName.ToLowerInvariant()[..1]
            : string.Empty;

        var firstLetterOfLastName = transliteratedLastName.Length > 0
            ? transliteratedLastName.ToLowerInvariant()[..1]
            : string.Empty;

        var username = $"{_prefix}{yearOfStudy}{firstLetterOfFirstName}{firstLetterOfMiddleName}{firstLetterOfLastName}";
        var password = _facultyNumber;

        return new Account(_firstName, _middleName, _lastName, username, password);
    }
}

public interface IFirstNameSelectionStage
{
    IPrefixSelectionStage WithFirstName(string firstName);
}

public interface IPrefixSelectionStage
{
    IPrefixSelectionStage WithMiddleName(string middleName);
    IPrefixSelectionStage WithLastName(string lastName);
    IFacultyNumberSelectionStage WithPrefix(string prefix);
}

public interface IFacultyNumberSelectionStage
{
    IBuildInitializerStage WithFacultyNumber(string facultyNumber);
}

public interface IBuildInitializerStage
{
    Account Build();
}

CodePudding user response:

I see two solutions possible here, one way is to leave the idea of single method per field aside and integrate all the name fields into one method, having some of them obligatory other optional.

public IPrefixSelectionStage WithName(string firstName, string middleName = null, string lastName = null)
{
    if (string.IsNullOrWhiteSpace(firstName))
    {
        throw new ArgumentOutOfRangeException(nameof(firstName), "Please provide a valid FirstName");
    }
    _firstName = firstName;

    if (!string.IsNullOrWhiteSpace(middleName))
         _middleName = middleName;
    if (!string.IsNullOrWhiteSpace(lastName))
         _lastName = lastName;
    
    return this;
}

The other approach would be to divide the interfaces further and then the FirstName method would return IPrefixOrOptionalNamesStage. MiddleName would return IPrefixOrLastNameStage and LastName would return IPrefixStage

public IPrefixOrOptionalNamesStage WithFirstName(string firstName)

public IPrefixOrLastNameStage WithMiddleName(string middleName)

public IPrefixStage WithLastName(string lastName)

public interface IPrefixOrOptionalNameStage
{
    IPrefixSelectionStage WithMiddleName(string middleName);
    IPrefixSelectionStage WithLastName(string lastName);
    IFacultyNumberSelectionStage WithPrefix(string prefix);
}

public interface IPrefixOrLastNameStage
{
    IPrefixSelectionStage WithLastName(string lastName);
    IFacultyNumberSelectionStage WithPrefix(string prefix);
}

In thast case you can only set each of them once and they still are optional, at the same time the order is fixed. That approach however quickly starts to be hard to maintain, as you add fields and dependencies from my experience

  • Related