Let's say I have an application where a user can do exercises and track them. I have the User entity:
class User {
private int stamina;
private int experience;
}
And the Exercise entity:
class Exercise {
private Date finishedAt;
}
The user can have many exercises. Once an excercise is done the user's experience gets higher and his stamina gets lower. If the user's stamina gets too low the user cannot do his exercises anymore for some time.
Implementation
Example one
The exercise has a reference to the user object.
class Exercise {
private User user;
private Date finishedAt;
public void finish() {
if (user.getStamina() <= 0) {
throw new Exception("The stamina is too low");
}
finishedAt = Date.now();
user.addExperience(calculateExperience());
user.removeStamina(calculateStamina())
}
private int calculateExperience() {
// some calculations based on the difficulty of the exercise and the user's current level
}
private int calculateStamina() {
// same as the experience
}
}
The problem
- A strong coupling. If I change the User class I need to change the Exercise class.
- A possible side-effect hiding. When a client calls the finish method on an Exercise object he doesn't know he changes the User object as well.
- The domain language. I can imagine a client can add only some experience by calling the addExperience method as a bonus, but I cannot imagine why a client call the removeStamina method. Of course he should not do it, but he can. Is this a bad interface?
Example two
The exercise as a parameter
class User {
public void finishExercise(Exercise exercise) {
if (stamina <= 0) {
throw new Exception();
}
stamina -= exercise.calculateStamina(user);
experience += exercise.calculateExperience(user);
exercise.markAsFinished();
}
}
class Exercise {
public void markAsFinished() {
finishedAt = Date.now();
}
}
The problem
- A client can give the other user's exercise object as a parameter. Where the validation of this case should be?
- Side-effects. When finishExercise is called the argument is changed too.
- Ambiguity. A client doesn't know which method he should call: finishExercise or markAsFinished. In fact he should not have an access to the second method at all.
Example three
The logic outside the entities
public static void main() {
Exercise exercise = exerciseRepository.findById(1);
User user = userRepository.findById(exercise.getUserId());
int stamina = exercise.calculateStamina(user);
int experience = exercise.calculateExperience(user);
user.addExperience(experience);
user.removeStamina(stamina);
exercise.markAsFinished();
}
The problem
- The domain language. What does removeStamina mean for a client?
- Consistency. A client can only call the markAsFinished method and think everything is done. He doesn't know he should do some calculations for the user object.
Example four
The logic inside the service
public static void main() {
Exercise exercise = exerciseRepository.findById(1);
User user = userRepository.findById(exercise.getUserId());
userService.finishExercise(user, exercise);
userRepository.save(user);
exerciseRepository.save(exercise);
}
The problem
- Ambiguity. A client can call the finishExercise method of the service as well as the markAsFinished method of the entity method.
- The domain language. A client has an access to meaningless (from his point of view) methods of the entities again.
The question
What solution would be correct?