You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

子结构依赖父属性:如何规范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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 04:42:06