Bitte diskutieren ob intern in den Controls mit Optional<Problem>
oder Exceptions
(ThrowableProblem
) gearbeitet werden soll.
Ich würde einfach das nehmen, was ihr besser findet. Kann mich mit beidem anfreunden, da das Thema nur klassen-intern sein dürfte. Eigentlich wollen wir ja Exceptions vermeiden, aber haben ja nicht gesagt wie weit wir das treiben. Drum herum kommen wir eh nicht weil es Exceptions gibt die wir halt catchen müssen. Und die Kommunikation von Control zu Endpoint würde ich definitiv ohne Exceptions machen.
Hier geht es um kleinere Scopes (klassenintern), da Java ja keine multiple-return-values kann und Wrapper-Objekte teilweise auch umständlich sein können.
In diesem Issue geht es um die Schneide bei Methoden, die in Vanilla-Java entweder ohne return value
durchlaufen oder eine Exception
schmeißen würden. Diese Methoden könnte man auch mit Optional
statt void & Exception
umschreiben.
Folgende Problematik (beim birthday
):
public static class SignUpMapper implements Mapper<User, SignUpDto> {
public static final SimpleDateFormat BIRTHDAY_FORMAT = new SimpleDateFormat("YYYY-MM-DD");
@Override
public User mapToEntity(SignUpDto signUpDto) {
User user = new User();
user.setEmail(signUpDto.getEmail());
user.setFirstName(signUpDto.getFirstName());
user.setLastName(signUpDto.getLastName());
try {
user.setBirthday(BIRTHDAY_FORMAT.parse(signUpDto.getBirthday()));
} catch (ParseException e) {
Problem problem = SignUpProblem.INVALID_BIRTHDAY.generateProblem(
Map.of("birthday", signUpDto.getBirthday()),
signUpDto.getBirthday()
);
throw new ThrowableProblem(problem, e);
}
return user;
}
}
Bei einer solchen Klasse würde ich mit einer Exception
arbeiten, da man sonst alle Mapper mit irgendwelchen Quittungsobjekten wrappen müsste.. Daher catche ich die ParseException
(was ich muss) und rethrowe sie als ThrowableProblem extends RuntimeException
(da ich die Methoden-Signatur nicht ändern kann).
Damit kann ich das in meiner aufrufenden Methode ganz entspannt fangen:
User user;
try {
user = new SignUpMapper().mapToEntity(data);
} catch (ThrowableProblem e) {
// failure branch: invalid birthday format
// this shouldn't happen if API is used correctly -> log.warn
log.warn("Invalid birthday format in sign-up: " + data.getBirthday(), e.getCause());
return result.failure(e.getProblem());
}
repository.add(user);
Jetzt hatten wir ja aber gesagt, dass wir Exceptions möglichst vermeiden wollen. Jetzt gibt es eine Validierungsmethode, die man entweder mit void
& Exception
oder Optional<Problem>
implementieren könnte.
Optional<Problem>
➕ keine Exceptions
➖ nicht einheitlich
Optional<Problem> checkIfEmailTaken() {
Specification<User> presentCheck = new UserByEmailSpec(data.getEmail());
Optional<User> referredUser = repository.queryFirst(presentCheck);
if (referredUser.isPresent()) {
// failure branch: user already exists
Problem problem = SignUpProblem.EMAIL_EXISTS.generateProblem(
Map.of("email", data.getEmail()),
data.getEmail()
);
return Optional.of(problem);
} else {
// success
return Optional.empty();
}
}
@Override
public <T> T execute(ResultBuilder<T, Object> result) {
Optional<Problem> emailTakenProblem = checkIfEmailTaken();
if (emailTakenProblem.isPresent()) {
return result.failure(emailTakenProblem.get());
}
// ...
}
void
& Exception
➕ einheitlich mit Exceptions
➖ Exception wollten wir möglichst vermeiden
void doCheckIfEmailTaken() throws ThrowableProblem {
Specification<User> presentCheck = new UserByEmailSpec(data.getEmail());
Optional<User> referredUser = repository.queryFirst(presentCheck);
if (referredUser.isPresent()) {
// failure branch: user already exists
Problem problem = SignUpProblem.EMAIL_EXISTS.generateProblem(
Map.of("email", data.getEmail()),
data.getEmail()
);
throw new ThrowableProblem(problem);
}
}
@Override
public <T> T execute(ResultBuilder<T, Object> result) {
try {
doCheckIfEmailTaken();
} catch (ThrowableProblem e) {
return result.failure(e.getProblem());
}
// ...
}