# Recipe 模块代码审阅报告 ## 一、架构概览 配方模块采用 MVVM 架构,分为四层配方体系:Carrier → Substrate → Wafer → Process,形成级联关系。使用 Stylet 框架 + JSON 文件持久化。 ```mermaid graph TD A[CarrierRecipe 载具配方] --> B[SubstrateRecipe 基板配方] B --> C[WaferRecipe 芯片配方] C --> D[ProcessRecipe 工艺配方] E[RecipeManager] --> A E --> B E --> C E --> D F[RecipeWrapManager] --> G[RecipeWrap 列表] H[RecipeViewModel] --> I[CarrierRecipeViewModel] H --> J[SubstrateRecipeViewModel] H --> K[WaferRecipeViewModel] H --> L[ProcessRecipeViewModel] ``` --- ## 二、发现的问题 ### 问题 1:ViewModel 层存在严重的代码重复(高优先级) `CarrierRecipeViewModel`、`SubstrateRecipeViewModel`、`WaferRecipeViewModel`、`ProcessRecipeViewModel` 四个类中,以下逻辑几乎完全相同: - `SelectedRecipeWrap` 属性的 setter 逻辑(离开检查 + 加载配方 + 快照) - `RegisterButtonGroupEvents()` 方法(18 行事件注册代码完全一致) - `TryLeaveCurrentRecipeContext()` 方法 - `SaveCurrentRecipe()` / `CaptureSavedSnapshot()` / `IsRecipeDirty()` / `SelectRecipeWrapInternal()` 方法 - `OnCreateNewRecipe` / `OnDeleteRecipe` / `OnOpenRecipe` / `OnSaveRecipe` / `OnClearRecipe` 等事件处理 **改进建议**:提取一个泛型基类 `RecipeViewModelBase where TRecipe : RecipeBase`,将公共逻辑下沉,子类只需覆写差异部分(如加载配方的具体类型、级联逻辑等)。 --- ### 问题 2:RecipeButtonGroupViewModel 在 ViewModel 中直接创建 View(中优先级) `RecipeButtonGroupViewModel.ShowRecipeWindow()` 方法直接 `new TWindow()`,违反了 MVVM 的核心原则——ViewModel 不应直接创建和管理 View。 **改进建议**:使用 Stylet 的 `IWindowManager.ShowDialog()` 来显示对话框窗口,或引入一个 `IDialogService` 接口来解耦。 --- ### 问题 3:RecipeNameWindowModel 与 RecipeReNameWindowModel 代码重复(中优先级) 这两个类的验证逻辑、`OkCommand`/`CancelCommand`、`UpdateValidationMessage()` 方法几乎完全一致,`RecipeReNameWindowModel` 仅多了一个 `OldRecipeName` 属性。 **改进建议**:让 `RecipeReNameWindowModel` 继承 `RecipeNameWindowModel`,或提取公共基类。 --- ### 问题 4:RecipeBase 同时实现 INotifyPropertyChanged 和继承 JsonFileWritableBase(中优先级) `RecipeBase` 手动实现了 `INotifyPropertyChanged`(包含 `SetProperty` 辅助方法),但其子类的子属性模型(如 `SubstrateInfo`、`MarkData` 等)继承的是 Stylet 的 `PropertyChangedBase`,使用 `SetAndNotify`。这导致项目中存在两套属性变更通知机制。 **改进建议**:统一通知机制。如果 `JsonFileWritableBase` 不能继承 `PropertyChangedBase`,考虑用组合代替继承,或让 `RecipeBase` 也使用 Stylet 的通知基础设施。 --- ### 问题 5:CameraBaseViewModel 放在 Models 目录下(低优先级) `CameraBaseViewModel` 是一个 ViewModel 基类,但放在了 `Recipe/Models/` 目录下,违反了项目的分层约定。 **改进建议**:移动到 `Recipe/ViewModel/` 目录。 --- ### 问题 6:命名拼写错误(低优先级) - `SubtrateMarkPars` → 应为 `SubstrateMarkPars`(缺少 `s`) - `SubtrateMarkParameterInfo` → 应为 `SubstrateMarkParameterInfo` - `ThickNess` → 应为 `Thickness`(大小写不一致) - `PIDOperater` → 应为 `PIDOperator` **改进建议**:统一修正拼写,使用 IDE 的重命名功能确保所有引用同步更新。 --- ### 问题 7:PIDOperater 使用反射遍历属性,且通过 IoC.Get 静态调用获取硬件(中优先级) `PIDOperater.SendPIDParameters()` 和 `ReadPIDParametersFromDevice()` 使用反射遍历所有属性来读写 PID 参数,性能不佳且不易维护。同时 `GetControler()` 通过 `IoC.Get()` 静态调用获取依赖,违反了依赖注入原则。 **改进建议**: - 将 `IMotionController` 或 `HardwareManager` 通过构造函数注入 - 考虑用显式的参数列表或字典代替反射遍历 --- ### 问题 8:RecipeManager 的删除操作缺少异常处理(中优先级) `DeleteCarrierRecipe` / `DeleteSubstrateRecipe` / `DeleteWaferRecipe` / `DeleteProcessRecipe` 直接调用 `Directory.Delete(path, true)` 而没有任何异常处理。如果目录不存在或被占用,会直接抛出异常。 **改进建议**:添加 try-catch 和存在性检查,返回操作结果或错误信息。 --- ### 问题 9:OnCopyRecipe / OnImportRecipe / OnExportRecipe 为空实现(低优先级) 多个 ViewModel 中的 `OnCopyRecipe`、`OnImportRecipe`、`OnExportRecipe` 方法体为空,功能未实现。 **改进建议**:要么实现这些功能,要么在 UI 上禁用对应按钮(通过 `CanExecute` 返回 false),避免用户点击后无反应。 --- ### 问题 10:CarrierRecipeViewModel.OnViewLoaded 中直接访问 RecipeWraps[0] 可能越界(高优先级) ```csharp // CarrierRecipeViewModel.cs:248 RecipeWraps[0].IsInUse = true; ``` 如果 `RecipeWraps` 为空集合,这行代码会抛出 `ArgumentOutOfRangeException`。 **改进建议**:添加空集合检查。 --- ### 问题 11:SubstrateRecipeViewModel.OnViewLoaded 中 SelectedRecipeWrap 可能为 null(高优先级) ```csharp // SubstrateRecipeViewModel.cs:498-499 SelectedRecipeWrap = RecipeWraps.Where(...).FirstOrDefault(); SelectedRecipeWrap.IsInUse = true; // 可能 NullReferenceException ``` `FirstOrDefault()` 可能返回 null,直接访问 `.IsInUse` 会崩溃。 **改进建议**:添加 null 检查。 --- ### 问题 12:RecipeWrapManager.Handle 方法中复用同一个 CarrierRecipe 实例(中优先级) ```csharp // RecipeWrapManager.cs:74 CarrierRecipe carrierRecipe = new CarrierRecipe(); foreach (var carrierWrap in CarrierRecipeWraps) { carrierRecipe.RecipeName = carrierWrap.RecipeName; carrierRecipe.Read(); ... } ``` 在循环中复用同一个 `CarrierRecipe` 实例,每次 `Read()` 会覆盖上一次的数据。虽然功能上可能没问题,但语义不清晰,且如果 `Read()` 失败可能导致残留旧数据。 **改进建议**:每次循环创建新实例。 --- ### 问题 13:MarkTeachViewModel 直接持有 Window 引用(中优先级) ```csharp private MarkCoordinatePreviewWindow _coordinatePreviewWindow; ``` ViewModel 直接持有 `Window` 实例引用,违反 MVVM 原则。 **改进建议**:使用 `IWindowManager` 管理窗口生命周期,或通过 Messenger/EventAggregator 通知 View 层打开窗口。 --- ### 问题 14:事件参数类散落在不同位置(低优先级) - `RecipeEventArgs` / `RecipeRenameEventArgs` 在 `Recipe/Models/RecipeEventArgs.cs` 中 - `SubstrateNameChangedEventArgs` 等在 `EventArgsFolder/RecipeNameChangedEventArgs.cs` 中,且命名空间为根命名空间 `MainShell` **改进建议**:统一事件参数的命名空间和存放位置,建议放在 `Recipe/Models/` 或专门的 `Recipe/Events/` 目录下。 --- ### 问题 15:SubstrateHeightMeasureSetting 和 SubstrateHeightMeasurePoint 未实现 IParameterItem(低优先级) 其他模型类(如 `SubstrateInfo`、`CarrierInfo`)都实现了 `IParameterItem` 接口以支持 `Clone()`,但 `SubstrateHeightMeasureSetting` 和 `SubstrateHeightMeasurePoint` 没有实现,可能导致序列化/克隆时行为不一致。 **改进建议**:统一实现 `IParameterItem` 接口。 --- ### 问题 16:ActiveRecipeService 未被使用(低优先级) `IActiveRecipeService` 和 `ActiveRecipeService` 定义了但在 Recipe 模块中未见使用。 **改进建议**:确认是否为未来预留,如果不需要则移除以减少代码噪音。 --- ## 三、改进优先级汇总 | 优先级 | 问题编号 | 简述 | |--------|---------|------| | 高 | 1 | 四个 RecipeViewModel 严重代码重复,提取泛型基类 | | 高 | 10 | CarrierRecipeViewModel.OnViewLoaded 数组越界风险 | | 高 | 11 | SubstrateRecipeViewModel.OnViewLoaded 空引用风险 | | 中 | 2 | ButtonGroupViewModel 直接创建 View | | 中 | 3 | 两个命名窗口 Model 代码重复 | | 中 | 4 | 两套属性变更通知机制并存 | | 中 | 7 | PIDOperater 反射 + 静态 IoC 调用 | | 中 | 8 | 删除操作缺少异常处理 | | 中 | 12 | RecipeWrapManager 循环中复用实例 | | 中 | 13 | MarkTeachViewModel 持有 Window 引用 | | 低 | 5 | CameraBaseViewModel 放错目录 | | 低 | 6 | 命名拼写错误 | | 低 | 9 | 空实现的操作方法 | | 低 | 14 | 事件参数类位置分散 | | 低 | 15 | 部分模型未实现 IParameterItem | | 低 | 16 | ActiveRecipeService 未使用 |