2

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

  1. A strong coupling. If I change the User class I need to change the Exercise class.
  2. 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.
  3. 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

  1. A client can give the other user's exercise object as a parameter. Where the validation of this case should be?
  2. Side-effects. When finishExercise is called the argument is changed too.
  3. 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

  1. The domain language. What does removeStamina mean for a client?
  2. 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

  1. Ambiguity. A client can call the finishExercise method of the service as well as the markAsFinished method of the entity method.
  2. 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?

1
  • The "correct" solution is the one that solves the problems you described. Many of those problems can be solved by applying sensible object-oriented techniques. I'm not sure that DDD has much to say about that; DDD is a design methodology, not a software development technique. Commented Jul 8, 2021 at 15:17

2 Answers 2

1

Instead of treating User and Exercise as two classes, you should think in terms of Aggregates and Entities.

Approach 1: If Exercise and User are both aggregates, they should not be connected. If a user completes an exercise, you should mark the Exercise as completed (looks like you are doing it by updating finishedAt) and then raise a Domain Event whose payload contains details of the user and the exercise completed.

User aggregate consumes the event and updates the stamina and experience in the user's record.

Approach 2: If Exercise is an entity enclosed within the aggregate, you should call a User method that updates the Exercise within the User aggregate. Manipulating the Exercise separately would be incorrect.

P.S I am assuming you mean "Workout" when you say "Exercise". Exercise would be something like Squats, while Workout would stand for the user actually doing squats on a given day. This nomenclature is fundamental to think about, as it exposes valuable relationships between entities.

5
  • Would you use domain events without an event bus of some kind (even C#'s built-in event system)? Do you think it can still be useful for one function to return an event and its caller service to manually pass it as an argument to the other function? Commented May 6 at 5:53
  • Server buses, Message brokers, and messaging mediums are technical implementations meant to increase systems' scalability and robustness, but there is nothing stopping us from using simple event passing if that serves our purpose. One thing that does get impacted is passing messages across services. Passing an event as an argument to another function assumes that the other function is part of the same runtime, which is usually not the case.
    – Subhash
    Commented May 8 at 2:10
  • If you go back to the origins of OOPS, even Alan Kay thought that objects should not have taken center stage in OOPS. It was meant to be messages: userpage.fu-berlin.de/~ram/pub/pub_jf47ht81Ht/doc_kay_oop_en.
    – Subhash
    Commented May 8 at 2:10
  • Thank you for the reply and the article. Regarding the runtime, yes, it does assume that, but I think that's fine. My reluctance to introduce a messaging medium stems from the belief that it's overkill for smaller projects, which are by nature single processes. Commented May 8 at 5:53
  • Not using messaging mediums for small projects is right on. Any technology that doesn't add value to that phase/requirements of the system should be avoided as long as possible (until the last responsible moment) 👍. If you haven't already, look up Actor Model Design Pattern. It is just one implementation of how a system can pass messages between objects, but it will give you ideas for implementation.
    – Subhash
    Commented May 8 at 21:50
0

Exercise exists in two states. Finished or not. Let's fix that by creating Workout which is always finished if it exists. A Workout is a finished Exercise.

Class Workout {
    private Exercise exercise;
    private Date finishedAt;
}

Now the only mutable class to contend with is User. And it could manage tracking changes to it's stamina and experience by keeping a collection of Workouts.

Another benefit is there's no need to throw an exception if the Exercise attempt fails. The result is that the User doesn't get a new Workout.

Will probably want to add Rest at some point so Users can recover stamina. Maybe User keeps a collection of Activitys.

1
  • Immutable objects are great, but I think I need the entity, because I also want the possibility to add the name, the description and tags to the existing exercises and change them too.
    – user397446
    Commented Jul 8, 2021 at 16:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.