SeatFlow.Application 层的外观模式、策略管道、命令模式、存储抽象和 DI 注册
本页目录
全部文档
Application 层详解
SeatFlow.Application 是 SeatFlow 的应用编排层,作为 UI 层和领域层之间的桥梁。它负责编排数据加载、策略执行、存储访问和命令历史。所有业务逻辑的协调入口均在这一层。
IApplicationFacade (外观接口)
IApplicationFacade 是 UI 层的单一入口点(外观模式),封装 40+ 方法,涵盖:
public interface IApplicationFacade
{
// 数据管理
Task<IReadOnlyList<StudentDatasetInfo>> ListStudentDatasetsAsync(CancellationToken ct = default);
Task ImportStudentsAsync(string filePath, CancellationToken ct = default);
// 排座执行
Task<SeatingWorkspace> GenerateSeatingAsync(SeatingRequest request,
IProgress<SeatingProgress>? progress = null, CancellationToken ct = default);
// 导出
Task ExportSeatingPlanAsync(ExportOptions options, SeatingWorkspace workspace,
ClassroomLayoutDefinition layout, CancellationToken ct = default);
// 快照
Task<IReadOnlyList<SeatingSnapshot>> GetSnapshotsAsync(string venueId, CancellationToken ct = default);
Task RollbackToSnapshotAsync(string snapshotId, CancellationToken ct = default);
// 命令历史(撤销/重做)
Task<bool> ExecuteCommandAsync(IUndoableCommand command, CancellationToken ct = default);
Task<bool> UndoAsync(CancellationToken ct = default);
Task<bool> RedoAsync(CancellationToken ct = default);
// 策略配置
Task<IReadOnlyList<StrategyDisplayInfo>> GetStrategiesAsync(CancellationToken ct = default);
Task SaveStrategyConfigAsync(string id, StrategyConfig config, CancellationToken ct = default);
// ... 更多方法
}
ViewModel 通过构造函数注入 IApplicationFacade,不直接依赖任何领域或基础设施类型。
StrategyExecutionPipeline (策略管道)
策略管道采用 Fill-in-Order 模型执行独立策略:
public class StrategyExecutionPipeline
{
public async Task<SeatingPlan> ExecuteAsync(
SeatingWorkspace workspace, CancellationToken cancellationToken)
{
// 按 Priority 降序执行每个启用的独立策略
foreach (var strategy in _strategies
.OrderByDescending(s => s.Priority)
.Where(s => s.IsEnabled))
{
var result = await strategy.ExecuteAsync(workspace, cancellationToken);
if (!result.Success)
{
workspace.LogError(strategy.Id, strategy.Name,
"Pipeline_ExecFailed", result.Message);
}
}
return workspace.BuildSeatingPlan();
}
}
执行流程(ApplicationFacade 在构建策略列表时已排除 visible=false 的策略,管道中仅处理已纳入的策略):
- 按 Priority 降序排列
IsEnabled为 true 的独立策略 - 依次执行每个独立策略,所有策略操作同一个
SeatingWorkspace - 依赖策略不在外部管道中执行——它们在
RandomFillStrategy的分配循环中按上下文内部优先级评估
RandomFill 上下文循环
RandomFillStrategy 作为依赖策略的宿主,其分配循环:
while 还有未分配学生 AND 空座位:
随机选 (student, seat)
rerollCount = 0
loop:
依次调用依赖策略 EvaluateAsync (按内部 Priority 降序)
if Reject → rerollCount++
if >= maxRerolls 兜底强制分配
else 换随机座位重试
if Handled → 依赖策略已自行分配,跳过 TryAssignSeat
if 全部 Approve → TryAssignSeat, 刷新列表
约束学生(DeskMate 分组)优先分配以减少重掷次数。逐出操作尊重先前策略(FixedSeat、FrontRowRotation)的分配结果。
命令模式
IUndoableCommand 和 CommandHistory 提供快照式撤销/重做:
public interface IUndoableCommand
{
string Description { get; }
Task<bool> ExecuteAsync(CancellationToken ct = default);
Task<bool> UndoAsync(CancellationToken ct = default);
}
CommandHistory 管理操作栈,支持撤销和重做导航。采用快照式实现——每个命令保存操作前后的状态快照,而非增量操作记录。
存储抽象与平台差异
Application 层通过 Core 层的 ILocalDataStore 抽象访问本地数据,不直接依赖文件系统。各启动壳在 DI 中注册平台实现:
| 平台 | 实现 | 存储介质 |
|---|---|---|
桌面(SeatFlow.Desktop) |
FileSystemDataStore |
应用数据目录文件系统 |
浏览器(SeatFlow.Browser) |
IndexedDbDataStore |
浏览器 IndexedDB(库 seatflow / 存储 files) |
统一通过该接口读写后,仓库、文件迁移、SeatSets 打包/解包等上层服务无需感知平台差异。平台专属能力由各壳实现并通过接口暴露:
- 数据迁移:桌面端支持
.seatsets文件关联与首次启动自动导入;浏览器端以打包上传/下载等价实现 - 文件导入导出:XLSX/CSV/JSON 在两端均可用;PDF 与图片导出仅桌面可用
- 数据目录设置:仅桌面端提供;浏览器数据保存在 IndexedDB 中
DI 注册
Application 层注册
ServiceCollectionExtensions.AddSeatFlowApplication(...) 注册所有 Application、Core 和 Infrastructure 层的服务。
注册的服务包括:
- 策略(内置 7 条策略 + 依赖策略)
- 导出器
- 数据提供者
- 仓库
- 策略执行管道
- 外观
启动壳注册
各启动壳在共享注册之外补充平台服务:
| 壳 | 补充注册 |
|---|---|
SeatFlow.Desktop |
FileSystemDataStore、桌面文件/对话框服务、WatchdogService |
SeatFlow.Browser |
IndexedDbDataStore、WebDialogService、WebFileService、BrowserConsoleLoggerProvider |
共享层 UI 服务与 ViewModel 同样在启动壳中注册:
services.AddSingleton<INavigationService, NavigationService>();
services.AddSingleton<IFileService, FileService>();
services.AddSingleton<IDialogService, DialogService>();
services.AddSingleton<MainShellViewModel>();
// 所有页面 ViewModel
services.AddSingleton<HomeViewModel>();
services.AddSingleton<MemberManagementViewModel>();
// ...
日志
- 使用 Serilog 4 +
Microsoft.Extensions.Logging.ILogger<T> - 日志输出到文件(
Serilog.Sinks.File) - Application 服务通过构造函数注入
ILogger<T> - 可选日志参数回退到
NullLogger<T>.Instance
浏览器端无法写入本地文件,由
BrowserConsoleLoggerProvider将日志转发到浏览器 DevTools Console。
public class StrategyExecutionPipeline
{
private readonly ILogger<StrategyExecutionPipeline> _logger;
public StrategyExecutionPipeline(
IEnumerable<ISeatingStrategy> strategies,
ILogger<StrategyExecutionPipeline>? logger = null)
{
_strategies = strategies;
_logger = logger ?? NullLogger<StrategyExecutionPipeline>.Instance;
}
}