C# 中如何在复杂的类组合层次中管理和传递大量参数?
建议保留构造函数注入,把配置数据和对象组装分开处理:用含义明确的配置对象组织参数,由工厂或组合根(Composition Root)创建底层对象,再将它们传给上层类。
用户仍然可以在入口处设置某个 Gear 的半径,Car 的构造函数只需接收它直接依赖的 Gearbox。这样,底层参数就不必经过每一层业务类的构造函数。
为什么会出现逐层传参
当 LevelOne 创建 LevelTwo,LevelTwo 又创建 LevelThree 时,每一层都负责组装下层对象,因此需要知道创建下层对象所需的参数。
对象之间存在组合关系,并不意味着它们必须负责创建彼此。一个对象可以持有另一个对象,由外部代码完成创建并传入:
var gear = new Gear(radiusMm);
var gearbox = new Gearbox(gear);
var car = new Car(gearbox);
Car 只需要知道自己依赖 Gearbox,无须了解齿轮半径。这些细节由负责组装对象的代码处理。
从 LevelOne 到 LevelTen 也是如此:先创建 LevelTen,再逐步组装上层对象。参数只出现在使用它的对象和相应的创建代码中。
三种方案怎么选
| 方案 | 主要影响 | 建议 |
|---|---|---|
| 全局静态字段 | 隐藏依赖,共享可变状态会影响不同任务、窗口和测试 | 不适合作为用户参数的默认存储方式 |
通过 GetInstance() 访问单例 | 仍然依赖全局访问入口,难以同时使用不同配置 | 不适合保存每次分析任务的参数 |
| 构造函数注入 | 依赖明确,便于独立创建和测试对象 | 保留,并调整参数分组和对象组装职责 |
用户可以修改的参数不适合命名为 Constants。const 表示编译期常量,static readonly 也不会自动解决全局依赖问题。固定的常量可以独立定义,用户设置则需要明确作用范围。
“单例生命周期”和“全局访问”也需要区分。通过构造函数注入共享实例,可以明确表达依赖;各个类自行调用 Settings.GetInstance(),则会隐藏依赖。即使使用依赖注入容器,也要根据配置属于整个应用、某个文档还是某次任务,决定它的生命周期。
先按用途整理参数
可以按领域含义定义 GearSettings、GearboxSettings、CarSettings 等类型。科学分析软件中也可以有 SolverSettings、CalibrationSettings。
分组要对应实际用途。把十五个互不相关的参数塞进一个 Context,只是缩短了签名,理解和维护起来仍然困难。
配置树也不必与对象树逐层对应。配置描述用户需要控制的内容,运行时对象还可能包含缓存、策略和辅助对象,这些细节通常无须暴露给用户。
处理流程可以按以下步骤安排:
- 从界面或配置文件收集设置,检查必填项、范围和单位。
- 由工厂读取设置,创建相关对象。
- 让业务对象通过构造函数接收自己的直接依赖。
- 在任务开始时确定本次使用的参数,避免界面在计算过程中修改这些参数。
C# 示例:配置齿轮半径,再创建 Car
配置类允许界面修改设置,创建后的领域对象通过只读属性保存状态。这个示例只有一个目标齿轮,实际项目可以按齿轮编号或用途扩展。
using System;
public sealed class GearSettings
{
public double RadiusMm { get; set; } = 25.0;
}
public sealed class GearboxSettings
{
public GearSettings TargetGear { get; set; } = new GearSettings();
}
public sealed class CarSettings
{
public GearboxSettings Gearbox { get; set; } = new GearboxSettings();
}
public sealed class Gear
{
public double RadiusMm { get; }
public Gear(double radiusMm)
{
if (double.IsNaN(radiusMm) ||
double.IsInfinity(radiusMm) ||
radiusMm <= 0)
{
throw new ArgumentOutOfRangeException(
nameof(radiusMm), "齿轮半径必须是有限的正数,单位为毫米。");
}
RadiusMm = radiusMm;
}
}
public sealed class Gearbox
{
public Gear TargetGear { get; }
public Gearbox(Gear targetGear)
{
TargetGear = targetGear
?? throw new ArgumentNullException(nameof(targetGear));
}
}
public sealed class Car
{
public Gearbox Gearbox { get; }
public Car(Gearbox gearbox)
{
Gearbox = gearbox
?? throw new ArgumentNullException(nameof(gearbox));
}
}
public static class CarFactory
{
public static Car Create(CarSettings settings)
{
if (settings == null)
throw new ArgumentNullException(nameof(settings));
var gearboxSettings = settings.Gearbox;
if (gearboxSettings == null)
throw new ArgumentException("缺少变速箱配置。", nameof(settings));
var gearSettings = gearboxSettings.TargetGear;
if (gearSettings == null)
throw new ArgumentException("缺少目标齿轮配置。", nameof(settings));
var gear = new Gear(gearSettings.RadiusMm);
var gearbox = new Gearbox(gear);
return new Car(gearbox);
}
}
在入口处收集用户输入,再创建对象:
var settings = new CarSettings();
settings.Gearbox.TargetGear.RadiusMm = 32.5;
Car car = CarFactory.Create(settings);
Car 的构造函数不需要齿轮半径参数,Gearbox 也无须转发这个值。CarFactory 负责读取配置和组装对象,Gear 自己校验半径。
CarFactory 是静态类,但没有保存可变的全局配置。每次调用都显式传入设置,因此可以创建参数不同的多个 Car。
层次很深时,拆分创建逻辑
对象数量增加后,可以让 CarFactory 调用 GearboxFactory,再由后者调用更细的工厂。每个工厂只接收自己需要的配置分支,例如:
Gearbox gearbox = gearboxFactory.Create(settings.Gearbox);
Car car = new Car(gearbox);
配置在创建代码之间传递,无须穿过每一层业务对象。顶层工厂可以只协调几个子工厂,将具体的创建细节交给它们处理。
依赖注入容器可以管理服务依赖和对象生命周期,参数如何分组仍需自行设计。对于用户输入、任务参数和运行时才确定的数据,使用显式工厂往往更容易表达。业务类如果为了减少构造函数参数而自行从 IServiceProvider 获取依赖,就会形成 Service Locator,再次隐藏依赖关系。
构造过程需要分阶段选择组件或填写参数时,可以考虑 Builder。一次调用就能创建完整对象的情况,普通工厂通常已经够用。
构造函数有十五个参数,是否一定要拆类
如果大部分参数只是继续传给子对象,应当调整组装职责;属于同一组算法设置的参数,可以用专门的参数对象组织。一个类直接依赖十五个独立服务时,则值得检查它是否承担了过多职责。
参数数量只能提示问题,不能单独决定设计。科学计算算法可能确实需要较多控制参数。这时应明确每个参数的含义、单位、默认值和有效范围,无须为了缩短签名强行拆类。
用户在运行中修改参数怎么办
示例中的 Gear 复制了半径值,因此创建完成后,继续修改 settings 不会改变已有齿轮。科学分析任务通常适合这种做法:一次计算使用固定配置,并将配置与结果一起保存,便于复现。
参数需要动态生效时,应明确何时生效,以及如何更新:
- 下次任务生效:保存新设置,在启动下一次任务时创建运行对象。
- 当前对象立即生效:提供有业务含义的更新方法,并处理校验、缓存失效和线程同步。
- 多个参数必须一起改变:先校验完整的新配置,再统一切换,避免任务读到修改到一半的状态。
后台计算期间,不应让任务无约束地共享读取界面中的可变设置。配置项较多时,可以在任务边界生成独立的不可变快照。只读属性本身并不能保证它引用的集合或子对象也不可变。
备注:内容仅供参考。
