You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

307 lines
9.2 KiB

# 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<string>();
}
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<IEnumerable<string>> ScanImageFilesAsync(string directoryPath, CancellationToken cancellationToken = default);
Task<ImageItem?> GetImageInfoAsync(string filePath, CancellationToken cancellationToken = default);
}
```
#### 3. Core/Interfaces/IFileDialogService.cs
```csharp
namespace AuroraDesk.Core.Interfaces;
public interface IFileDialogService
{
Task<string?> 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<LoadImagesUseCase> _logger;
public async Task<LoadImagesResult> ExecuteAsync(string directoryPath, IProgress<LoadProgress> progress, CancellationToken cancellationToken = default)
{
// 业务逻辑:扫描文件、批量加载等
}
}
```
#### 5. Infrastructure/Services/ImageFileService.cs
```csharp
namespace AuroraDesk.Infrastructure.Services;
public class ImageFileService : IImageFileService
{
// 实现文件系统操作
public async Task<IEnumerable<string>> 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<string?> 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<ImageGalleryPageViewModel>? _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 中
- [ ] 所有服务通过接口抽象
- [ ] 依赖方向正确(指向内层)
- [ ] 没有循环依赖