Search code examples
objectoopdesign-patternsarchitecturedependencies

Child structure depends on parent property


I have some interconnected entities:

Exercises

For instance push ups, pull ups, squats. Each exercise has a name, the muscles involved (chest, legs ...), the executions steps and the category type.

Exercise category

It is an enum that represents how is this exercise measured.

  • For instance "push up" is a bodyweightReps type as you need to measure only the repetitions performed.
  • "Barbell squat" is a weightReps type as you record both weight on the barbell and the repetitions performed.
  • Each exercise can therefore be measured with different parameters and with a different number of parameters:
    • bodyweightReps needs to record only 1 parameter repetition,
    • while weightReps needs 2, 1 for weight and 1 for repetition).
  • For the moment all exercises can be measured with maximum 2 parameters, but that could change in the future

Workout activity

It is an activity performed in a workout. It has an exercise, some notes and a list of workout sets performed

Workout set

It is a set of a workout activity. It has a rest time and a list of measures for the values stored (for instance number of repetitions performed, weight used ...).
The measures of the workout set depends on the parent Workout activity Exercise Category, as a set of a Workout Activity with "Barbell Squat", needs to measure the weight and the repetitions performed, as the exercise category is weightReps.

As you can see, a WorkoutActivity has an exercise, and the structure of the child WorkoutSet measures property depends on the parent WorkoutActivity Exercise category property.

enum ExerciseType {
 weightReps,
 bodyWeightReps,
 distance,
 time
}

class Exercise {
 String name;
 List<String> executionSteps;
 ExerciseType category;
}

class Workout {
 String name
 List<WorkoutActivity> activities;
}

class WorkoutActivity {
 String note;
 Exercise exercise;
 List<WorkoutSet> sets;
}

class WorkoutSet {
 int restTime;
 List<double> measures;
}

Problem

The above structure doesn't guarantee that the WorkoutSets of a workout activity whose exercise category type is bodyweightReps have only one measure nor that if the category type is weightReps they have 2 measures.

This issue raises some connected problems that can't be ignored.

Is there a better way to structure the entities so that the measures of the child WorkoutSet can be forced to follow the parent Workout Activity Exercise Category type structure?


Solution

  • In .NET 5 a couple of new code analysis attributes have been introduced, like MemberNotNullWhen. Which could be helpful if you would have dedicated fields inside the WorkoutSet for weight and repetition measures.

    Since we have a collection of measures and we are talking about different programming language, let me start with a simple example.

    For the sake of simplicity I moved the ExerciseType to the WorkoutSet

    abstract class WorkoutSet 
    {
        ExerciseType category;
        int restTime;
        List<double> measures;
        
        public sealed class WeightReps : WorkoutSet
        {
            public WeightReps (int restSeconds, double repetition, double weight )
            {
                this.category = ExerciseType.weightReps;
                this.restTime = restSeconds;
                this.measures = new() { repetition, weight };
            }
        }
    
        public sealed class BodyWeightReps : WorkoutSet
        {
            public BodyWeightReps (int restSeconds, double repetition )
            {
                this.category = ExerciseType.bodyWeightReps;
                this.restTime = restSeconds;
                this.measures = new() { repetition };
            }
        }
    }
    

    This would allow you to create either a WeightReps or a BodyWeightReps class. Their constructors are receiving the required number of parameters and they know how to initialize a given instance. You can not directly create a WorkoutSet.

    new WorkoutSet.BodyWeightReps((int)data.rest.TotalSeconds, data.repetition)
    
    new WorkoutSet.WeightReps((int)data.rest.TotalSeconds, data.repetition, data.weight);
    

    In case of C# you can push this idea a bit further with implicit operators to convert automatically a ValueTuple to a WorkoutSet derived class.

    public static implicit operator WorkoutSet( (TimeSpan rest, double repetition) data )
        => new WorkoutSet.BodyWeightReps((int)data.rest.TotalSeconds, data.repetition);
    
    public static implicit operator WorkoutSet( (TimeSpan rest, double repetition, double weight ) data )
        => new WorkoutSet.WeightReps((int)data.rest.TotalSeconds, data.repetition, data.weight);
    

    usage

    WorkoutSet bodyWeightReps = (TimeSpan.FromMinutes(0.5), repetition: 1.0d);
    WorkoutSet weightReps = (TimeSpan.FromSeconds(42), repetition: 1.0d, weight: 2.0d);
    

    Here you can find a working example: https://dotnetfiddle.net/P4rf2P