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

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.ApplicationTopLevelStorageProvider
  • 这些是平台特定的 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 直接使用 DirectoryFileInfo 等文件系统 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(必须修复)

  1. 创建接口抽象

    • Core/Interfaces/IImageFileService.cs
    • Core/Interfaces/IFileDialogService.cs
  2. 移动实体类

    • ImageItemCore/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 中
  • 所有服务通过接口抽象
  • 依赖方向正确(指向内层)
  • 没有循环依赖