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.
9.2 KiB
9.2 KiB
ImageGalleryPageViewModel Clean Architecture 检查报告
📋 检查日期
2025年1月
🔍 当前实现分析
❌ 违反 Clean Architecture 原则的问题
1. 直接依赖平台特定 API(严重违反)
问题位置:ImageGalleryPageViewModel.cs 第 108-147 行
// ❌ 直接使用 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 行
// ❌ 直接使用 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 行
// ❌ 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 行
// ❌ 直接使用 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
namespace AuroraDesk.Core.Entities;
public class ImageItem : ReactiveObject
{
// 实体属性
}
2. Core/Interfaces/IImageFileService.cs
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
namespace AuroraDesk.Core.Interfaces;
public interface IFileDialogService
{
Task<string?> SelectDirectoryAsync(string? initialDirectory = null, CancellationToken cancellationToken = default);
}
4. Application/UseCases/LoadImagesUseCase.cs
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
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
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(重构后)
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(必须修复)
-
创建接口抽象:
Core/Interfaces/IImageFileService.csCore/Interfaces/IFileDialogService.cs
-
移动实体类:
ImageItem→Core/Entities/ImageItem.cs
-
实现基础设施服务:
Infrastructure/Services/ImageFileService.csInfrastructure/Services/FileDialogService.cs
优先级 P1(建议修复)
-
创建应用用例:
Application/UseCases/LoadImagesUseCase.csApplication/UseCases/SelectDirectoryUseCase.cs
-
重构 ViewModel:
- 移除所有平台特定代码
- 移除业务逻辑
- 只保留状态管理和协调
优先级 P2(可选优化)
- 抽象 UI 线程调度:
- 创建
ISynchronizationContext接口 - 在 Infrastructure 层实现
- 创建
📝 参考实现
参考项目中 IconsPageViewModel 的实现方式:
- ✅ 通过依赖注入使用
IIconService接口 - ✅ 接口定义在 Core 层
- ✅ 实现在 Infrastructure 层
- ✅ ViewModel 只负责状态管理和协调
✅ 符合 Clean Architecture 的检查清单
- 所有平台特定代码都在 Infrastructure 层
- ViewModel 只依赖 Core 层的接口
- 业务逻辑在 Application 层(UseCase)
- 实体类在 Core/Entities 中
- 所有服务通过接口抽象
- 依赖方向正确(指向内层)
- 没有循环依赖