子结构依赖父属性:如何规范WorkoutSet与ExerciseCategory的匹配结构?
问题描述
实体定义
练习(Exercises)
- 示例:俯卧撑(push ups)、引体向上(pull ups)、深蹲(squats)
- 属性:名称、涉及肌肉群、执行步骤、类别类型
练习类别(Exercise category)
枚举类型,定义练习的测量规则:
bodyweightReps:仅需记录重复次数(1个参数)weightReps:需记录重量和重复次数(2个参数)distance:记录距离(1个参数)time:记录时长(1个参数)
注:当前最多支持2个参数,未来可能扩展
训练活动(Workout activity)
包含对应练习、备注、一组训练组(WorkoutSet)
训练组(Workout set)
包含休息时间、测量值列表,测量值结构依赖对应练习的类别
现有C#代码结构
enum ExerciseType { weightReps, bodyWeightReps, distance, time } class Exercise { public string Name { get; set; } public List<string> ExecutionSteps { get; set; } public ExerciseType Category { get; set; } public List<string> TargetMuscles { get; set; } } class Workout { public string Name { get; set; } public List<WorkoutActivity> Activities { get; set; } } class WorkoutActivity { public string Note { get; set; } public Exercise Exercise { get; set; } public List<WorkoutSet> Sets { get; set; } } class WorkoutSet { public int RestTime { get; set; } public List<double> Measures { get; set; } }
核心问题
现有结构无法强制约束:
bodyweightReps类别的练习,其训练组只能有1个测量值weightReps类别的练习,其训练组必须有2个测量值
导致类型安全缺失,容易出现数据不匹配的错误
解决方案
以下是几种强类型约束的设计方案,从编译期或运行层面保证测量值结构与练习类别匹配:
方案1:泛型+标记接口(编译期强约束)
通过泛型将练习类别与训练组的测量结构绑定,用标记接口区分不同测量需求:
// 标记接口,用于统一不同测量类型 public interface IMeasure { } // 对应单值测量类型(bodyweightReps、distance、time) public record SingleMeasure(double Value) : IMeasure; // 对应重量+次数的双值测量类型(weightReps) public record WeightRepMeasure(double Weight, double Repetitions) : IMeasure; // 泛型练习类,绑定对应的测量类型 public class Exercise<TMeasure> where TMeasure : IMeasure { public string Name { get; set; } public List<string> TargetMuscles { get; set; } public List<string> ExecutionSteps { get; set; } public ExerciseType Category { get; set; } } // 泛型训练活动,绑定练习的测量类型 public class WorkoutActivity<TMeasure> where TMeasure : IMeasure { public string Note { get; set; } public Exercise<TMeasure> Exercise { get; set; } public List<WorkoutSet<TMeasure>> Sets { get; set; } } // 泛型训练组,使用绑定的测量类型 public class WorkoutSet<TMeasure> where TMeasure : IMeasure { public int RestTime { get; set; } public TMeasure Measure { get; set; } } // 非泛型Workout,支持混合不同类型的训练活动 public class Workout { public string Name { get; set; } public List<object> Activities { get; set; } } // 可选:定义非泛型接口统一处理不同类型活动 public interface IWorkoutActivity { string Note { get; } ExerciseBase Exercise { get; } } public abstract class ExerciseBase { public string Name { get; set; } public List<string> TargetMuscles { get; set; } public List<string> ExecutionSteps { get; set; } public ExerciseType Category { get; set; } } public class Exercise<TMeasure> : ExerciseBase where TMeasure : IMeasure { } public class WorkoutActivity<TMeasure> : IWorkoutActivity where TMeasure : IMeasure { public string Note { get; set; } public Exercise<TMeasure> Exercise { get; set; } public List<WorkoutSet<TMeasure>> Sets { get; set; } ExerciseBase IWorkoutActivity.Exercise => Exercise; }
优缺点
- 优点:编译期强制检查,完全避免测量值不匹配问题;扩展性强,新增测量类型只需新增
IMeasure实现;语义清晰,测量数据有明确属性而非模糊的List<double> - 缺点:泛型引入后,集合处理需要额外接口/基类统一,代码量略有增加
方案2:基于继承的类型分层
为不同类别的练习和训练组创建具体子类,通过继承实现约束:
public enum ExerciseType { weightReps, bodyWeightReps, distance, time } // 抽象练习基类 public abstract class Exercise { public string Name { get; set; } public List<string> TargetMuscles { get; set; } public List<string> ExecutionSteps { get; set; } public abstract ExerciseType Category { get; } } // 单值练习子类(对应bodyweightReps、distance、time) public class SingleValueExercise : Exercise { public override ExerciseType Category { get; } public SingleValueExercise(ExerciseType category) { if (category != ExerciseType.bodyWeightReps && category != ExerciseType.distance && category != ExerciseType.time) { throw new ArgumentException("Invalid category for SingleValueExercise"); } Category = category; } } // 重量次数练习子类(对应weightReps) public class WeightRepExercise : Exercise { public override ExerciseType Category => ExerciseType.weightReps; } // 抽象训练组基类 public abstract class WorkoutSet { public int RestTime { get; set; } } // 单值训练组 public class SingleValueWorkoutSet : WorkoutSet { public double Value { get; set; } } // 重量次数训练组 public class WeightRepWorkoutSet : WorkoutSet { public double Weight { get; set; } public double Repetitions { get; set; } } // 抽象训练活动基类 public abstract class WorkoutActivity { public string Note { get; set; } public abstract Exercise Exercise { get; } public abstract List<WorkoutSet> Sets { get; } } public class SingleValueWorkoutActivity : WorkoutActivity { public override SingleValueExercise Exercise { get; set; } public override List<SingleValueWorkoutSet> Sets { get; set; } } public class WeightRepWorkoutActivity : WorkoutActivity { public override WeightRepExercise Exercise { get; set; } public override List<WeightRepWorkoutSet> Sets { get; set; } } public class Workout { public string Name { get; set; } public List<WorkoutActivity> Activities { get; set; } }
优缺点
- 优点:语义清晰,子类对应明确的练习类型;编译期约束训练组与练习类型匹配;无需泛型,符合传统继承设计习惯
- 缺点:新增练习类别时需同步新增多个子类,代码冗余度高;统一处理不同类型活动时需要类型转换
方案3:C#记录类型+运行时验证(轻量方案)
使用C# 9+的记录类型简化测量数据定义,在训练活动构造函数中做运行时约束:
public enum ExerciseType { weightReps, bodyWeightReps, distance, time } public record Exercise { public string Name { get; init; } public List<string> TargetMuscles { get; init; } public List<string> ExecutionSteps { get; init; } public ExerciseType Category { get; init; } } // 记录类型定义不同测量结构 public record SingleMeasure(double Value); public record WeightRepMeasure(double Weight, double Repetitions); public record WorkoutSet { public int RestTime { get; init; } public object Measure { get; init; } } public record WorkoutActivity { public string Note { get; init; } public Exercise Exercise { get; init; } public List<WorkoutSet> Sets { get; init; } // 构造函数中验证测量类型与练习类别匹配 public WorkoutActivity(string note, Exercise exercise, List<WorkoutSet> sets) { Note = note; Exercise = exercise ?? throw new ArgumentNullException(nameof(exercise)); Sets = sets ?? throw new ArgumentNullException(nameof(sets)); foreach (var set in sets) { if (!IsMeasureMatching(set.Measure, exercise.Category)) { throw new ArgumentException($"Measure type does not match exercise category: {exercise.Category}"); } } } private bool IsMeasureMatching(object measure, ExerciseType category) { return category switch { ExerciseType.bodyWeightReps or ExerciseType.distance or ExerciseType.time => measure is SingleMeasure, ExerciseType.weightReps => measure is WeightRepMeasure, _ => throw new NotSupportedException($"Unsupported category: {category}") }; } } public record Workout { public string Name { get; init; } public List<WorkoutActivity> Activities { get; init; } }
优缺点
- 优点:代码简洁,利用记录类型减少冗余;适合快速原型或小型项目
- 缺点:约束是运行时的,编译期无法提前发现错误;处理测量数据时需要类型转换,存在一定安全风险
方案选择建议
- 追求最高类型安全和扩展性:优先选方案1(泛型+标记接口),编译期杜绝不匹配问题,未来扩展更灵活
- 习惯传统继承设计:选择方案2,语义清晰但代码冗余度稍高
- 小型项目/快速迭代:方案3的简洁性更有优势
内容的提问来源于stack exchange,提问作者salim.elkh
相关产品推荐
相关产品推荐

