# ImageGalleryPageViewModel Clean Architecture 检查报告 ## 📋 检查日期 2025年1月 ## 🔍 当前实现分析 ### ❌ 违反 Clean Architecture 原则的问题 #### 1. **直接依赖平台特定 API(严重违反)** **问题位置**:`ImageGalleryPageViewModel.cs` 第 108-147 行 ```csharp // ❌ 直接使用 Avalonia 平台特定 API var app = Avalonia.Application.Current; if (app?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { topLevel = desktop.MainWindow; } var folders = await topLevel.StorageProvider.OpenFolderPickerAsync(...); ``` **问题分析**: - ViewModel 直接依赖 `Avalonia.Application`、`TopLevel`、`StorageProvider` - 这些是平台特定的 API,违反了依赖倒置原则 - ViewModel 应该依赖抽象接口,而不是具体实现 **应该的做法**: - 在 Core 层定义 `IFileDialogService` 接口 - 在 Infrastructure 层实现该接口(使用 Avalonia API) - ViewModel 通过依赖注入使用接口 #### 2. **直接使用文件系统操作(违反分层原则)** **问题位置**:`ImageGalleryPageViewModel.cs` 第 152-247 行 ```csharp // ❌ 直接使用 System.IO var imageFiles = await Task.Run(() => { if (!Directory.Exists(directoryPath)) { return new List(); } return Directory.EnumerateFiles(directoryPath, "*.*", SearchOption.AllDirectories) .Where(file => SupportedImageExtensions.Contains(...)) .ToList(); }); ``` **问题分析**: - ViewModel 直接使用 `Directory`、`FileInfo` 等文件系统 API - 文件系统操作属于基础设施层职责 - 业务逻辑(文件过滤、批量加载)应该在 Application 层或 Infrastructure 层 **应该的做法**: - 在 Core 层定义 `IImageFileService` 接口 - 在 Infrastructure 层实现文件扫描、过滤逻辑 - 在 Application 层创建 UseCase(如 `LoadImagesUseCase`)协调业务逻辑 #### 3. **实体类位置错误(违反分层原则)** **问题位置**:`ImageGalleryPageViewModel.cs` 第 258-304 行 ```csharp // ❌ ImageItem 应该放在 Core.Entities 中 public class ImageItem : ReactiveObject { // ... } ``` **问题分析**: - `ImageItem` 是领域实体,应该放在 Core/Entities 层 - 当前在 Presentation 层,违反了 Clean Architecture 的分层原则 **应该的做法**: - 将 `ImageItem` 移动到 `AuroraDesk.Core.Entities` 命名空间 - ViewModel 只负责状态管理和协调 #### 4. **业务逻辑在 ViewModel 中(违反单一职责原则)** **问题位置**:`ImageGalleryPageViewModel.cs` 第 152-247 行 **问题分析**: - 文件扫描、过滤、批量加载等业务逻辑都在 ViewModel 中 - ViewModel 应该只负责: - 状态管理(UI 状态) - 协调服务调用 - 数据绑定 **应该的做法**: - 创建 `LoadImagesUseCase` 在 Application 层 - 将业务逻辑移到 UseCase 中 - ViewModel 只调用 UseCase #### 5. **使用 Dispatcher.UIThread(平台特定代码)** **问题位置**:`ImageGalleryPageViewModel.cs` 第 199 行 ```csharp // ❌ 直接使用 Avalonia 的 Dispatcher await Dispatcher.UIThread.InvokeAsync(() => { ... }); ``` **问题分析**: - `Dispatcher.UIThread` 是 Avalonia 平台特定的 API - 应该通过接口抽象,或者使用更通用的方式 **应该的做法**: - 在 Core 层定义 `ISynchronizationContext` 或类似接口 - 在 Infrastructure 层实现(使用 Avalonia Dispatcher) - 或者,将 UI 更新逻辑移到 Application 层,通过回调返回结果 ## ✅ 正确的 Clean Architecture 结构 ### 建议的架构重构 ``` Core 层(无依赖) ├── Entities/ │ └── ImageItem.cs ✅ 实体类 └── Interfaces/ ├── IImageFileService.cs ✅ 图片文件服务接口 └── IFileDialogService.cs ✅ 文件对话框服务接口 Application 层(依赖 Core) └── UseCases/ └── LoadImagesUseCase.cs ✅ 加载图片用例 └── SelectDirectoryUseCase.cs ✅ 选择目录用例 Infrastructure 层(依赖 Core) └── Services/ ├── ImageFileService.cs ✅ 实现 IImageFileService └── FileDialogService.cs ✅ 实现 IFileDialogService(使用 Avalonia API) Presentation 层(依赖 Core 和 Application) └── ViewModels/ └── ImageGalleryPageViewModel.cs ✅ 只负责状态管理和协调 ``` ### 重构后的代码结构 #### 1. Core/Entities/ImageItem.cs ```csharp namespace AuroraDesk.Core.Entities; public class ImageItem : ReactiveObject { // 实体属性 } ``` #### 2. Core/Interfaces/IImageFileService.cs ```csharp namespace AuroraDesk.Core.Interfaces; public interface IImageFileService { Task> ScanImageFilesAsync(string directoryPath, CancellationToken cancellationToken = default); Task GetImageInfoAsync(string filePath, CancellationToken cancellationToken = default); } ``` #### 3. Core/Interfaces/IFileDialogService.cs ```csharp namespace AuroraDesk.Core.Interfaces; public interface IFileDialogService { Task SelectDirectoryAsync(string? initialDirectory = null, CancellationToken cancellationToken = default); } ``` #### 4. Application/UseCases/LoadImagesUseCase.cs ```csharp namespace AuroraDesk.Application.UseCases; public class LoadImagesUseCase { private readonly IImageFileService _imageFileService; private readonly ILogger _logger; public async Task ExecuteAsync(string directoryPath, IProgress progress, CancellationToken cancellationToken = default) { // 业务逻辑:扫描文件、批量加载等 } } ``` #### 5. Infrastructure/Services/ImageFileService.cs ```csharp namespace AuroraDesk.Infrastructure.Services; public class ImageFileService : IImageFileService { // 实现文件系统操作 public async Task> ScanImageFilesAsync(string directoryPath, CancellationToken cancellationToken = default) { // 使用 System.IO 进行文件扫描 } } ``` #### 6. Infrastructure/Services/FileDialogService.cs ```csharp namespace AuroraDesk.Infrastructure.Services; public class FileDialogService : IFileDialogService { // 使用 Avalonia API 实现文件对话框 public async Task SelectDirectoryAsync(string? initialDirectory = null, CancellationToken cancellationToken = default) { // 使用 TopLevel.StorageProvider.OpenFolderPickerAsync } } ``` #### 7. Presentation/ViewModels/ImageGalleryPageViewModel.cs(重构后) ```csharp namespace AuroraDesk.Presentation.ViewModels.Pages; public class ImageGalleryPageViewModel : RoutableViewModel { private readonly LoadImagesUseCase _loadImagesUseCase; private readonly SelectDirectoryUseCase _selectDirectoryUseCase; private readonly ILogger? _logger; // 只负责状态管理和协调 private async Task SelectDirectoryAsync() { var directory = await _selectDirectoryUseCase.ExecuteAsync(); if (directory != null) { await LoadImagesAsync(directory); } } private async Task LoadImagesAsync(string directoryPath) { // 调用 UseCase,接收进度更新 await _loadImagesUseCase.ExecuteAsync(directoryPath, progress => { // 更新 UI 状态 }); } } ``` ## 📊 违反原则总结 | 违反项 | 严重程度 | 位置 | 修复优先级 | |--------|---------|------|-----------| | 直接依赖平台 API | 🔴 严重 | SelectDirectoryAsync | P0 | | 直接使用文件系统 | 🔴 严重 | LoadImagesFromDirectoryAsync | P0 | | 实体类位置错误 | 🟡 中等 | ImageItem 类 | P1 | | 业务逻辑在 ViewModel | 🟡 中等 | LoadImagesFromDirectoryAsync | P1 | | 使用 Dispatcher.UIThread | 🟢 轻微 | UI 更新逻辑 | P2 | ## 🎯 重构建议 ### 优先级 P0(必须修复) 1. **创建接口抽象**: - `Core/Interfaces/IImageFileService.cs` - `Core/Interfaces/IFileDialogService.cs` 2. **移动实体类**: - `ImageItem` → `Core/Entities/ImageItem.cs` 3. **实现基础设施服务**: - `Infrastructure/Services/ImageFileService.cs` - `Infrastructure/Services/FileDialogService.cs` ### 优先级 P1(建议修复) 1. **创建应用用例**: - `Application/UseCases/LoadImagesUseCase.cs` - `Application/UseCases/SelectDirectoryUseCase.cs` 2. **重构 ViewModel**: - 移除所有平台特定代码 - 移除业务逻辑 - 只保留状态管理和协调 ### 优先级 P2(可选优化) 1. **抽象 UI 线程调度**: - 创建 `ISynchronizationContext` 接口 - 在 Infrastructure 层实现 ## 📝 参考实现 参考项目中 `IconsPageViewModel` 的实现方式: - ✅ 通过依赖注入使用 `IIconService` 接口 - ✅ 接口定义在 Core 层 - ✅ 实现在 Infrastructure 层 - ✅ ViewModel 只负责状态管理和协调 ## ✅ 符合 Clean Architecture 的检查清单 - [ ] 所有平台特定代码都在 Infrastructure 层 - [ ] ViewModel 只依赖 Core 层的接口 - [ ] 业务逻辑在 Application 层(UseCase) - [ ] 实体类在 Core/Entities 中 - [ ] 所有服务通过接口抽象 - [ ] 依赖方向正确(指向内层) - [ ] 没有循环依赖