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
307 lines
9.2 KiB
|
1 month ago
|
# 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 中
|
||
|
|
- [ ] 所有服务通过接口抽象
|
||
|
|
- [ ] 依赖方向正确(指向内层)
|
||
|
|
- [ ] 没有循环依赖
|
||
|
|
|