Sampeteq
Sampeteq

Reputation: 113

Data validation and account creation with Either - how to write it better?

I have an AccountCreator class with a create method that takes a DTO with the data needed to create an account. At the beginning there is an attempt to create 2 value objects (UserName and Password), then validate the uniqueness of the user name, create the Account entity which takes these 2 value objects in the constructor and save it in the repo. Of course, errors such as incorrect password length, etc. may be returned. I used Eithers for this and now the question is whether this code is ok or maybe it can be written somehow better?

public Either<Error, AccountDto> create(AccountCreateDto accountCreateDto) {

    var errorType = ErrorType.ACCOUNT_PERSISTENCE_ERROR;
    var errorMessage = "Not unique user name: " + accountCreateDto.userName;
    var error = new Error(errorType, errorMessage);

    return UserName
        .create(accountCreateDto.userName)
        .flatMap(userName ->
            userNameUniquenessChecker.isUnique(userName.text) ?
                Password
                    .create(accountCreateDto.password)
                    .flatMap(password -> {
                        var createdAccount = new Account(
                            userName,
                            password,
                            AccountStatus.OPEN,
                            LocalDateTime.now(),
                            new ArrayList<>()
                        );
                        var addedAccount = accountRepository.add(createdAccount);
                        var accountDto = new AccountDto(
                            addedAccount.userName.text,
                            addedAccount.password.text,
                            addedAccount.status,
                            addedAccount.creationDate,
                            (long) addedAccount.tasks.size()
                        );
                        return Either.right(accountDto);
                    }) : Either.left(error));
}

Upvotes: 1

Views: 140

Answers (1)

jon hanson
jon hanson

Reputation: 9408

The customary FP approach would be to use a Validation style applicative functor - there's one in Vavr called Validation. You can use Validation.combine to combine multiple Validation values into one, e.g.:

public Validation<Seq<<Error>, AccountDto> create(AccountCreateDto accountCreateDto) {
    Validation<Seq<<Error>, Account> validAcc =
            Validation.combine(
                    UserName.create(accountCreateDto.userName),
                    Password.create(accountCreateDto.password)
            ).ap((un, pw) -> new Account(
                    un,
                    pw,
                    AccountStatus.OPEN,
                    LocalDateTime.now()
            );

    Validation<Seq<<Error>, Account> validAcc2 =
            validAcc.flatMap(acc -> validateUserIdIsUnique(acc));
            
    Validation<<Seq<Error>, AccountDto> validAccDto =
            validAcc2.map(accountRepository::add)
                    .map(addedAccount -> 
                            new AccountDto(
                                    addedAccount.userName.text,
                                    addedAccount.password.text,
                                    addedAccount.status,
                                    addedAccount.creationDate,
                                    (long) addedAccount.tasks.size()
                            )
                    );

    }

    return validAccDto;
}

private static final var errorType = ErrorType.ACCOUNT_PERSISTENCE_ERROR;
private static final var errorMessage = "Not unique user name: " + accountCreateDto.userName;
private static final var error = new Error(errorType, errorMessage);

Validation<Seq<Error>, Account> validateUserIdIsUnique(Account acc) {
    return userNameUniquenessChecker.isUnique(acc.userName.text) ?
            Validation.valid(userName) :
            Validation.invalid(error);
}

You can elide the temporaries - validAcc, validAcc2 & validAccDto, however I left them in for clarity.

(caveat emptor - haven't tested that this code works, or even compiles)

Upvotes: 1

Related Questions