CleanArchitecture备忘录模式:对象状态保存
还在为对象状态回滚而头疼?一文掌握CleanArchitecture中备忘录模式的优雅实现!
在复杂的企业应用中,对象状态管理是一个常见但容易被忽视的挑战。当用户需要撤销操作、恢复历史状态或实现事务性操作时,备忘录模式(Memento Pattern)提供了完美的解决方案。本文将深入探讨如何在CleanArchitecture架构中优雅地实现备忘录模式。
📋 读完本文你将收获
- ✅ 备忘录模式的核心概念与适用场景
- ✅ CleanArchitecture中备忘录模式的架构设计
- ✅ 完整的代码实现与最佳实践
- ✅ 状态恢复的性能优化策略
- ✅ 与领域事件、CQRS模式的集成方案
🎯 备忘录模式核心概念
备忘录模式属于行为型设计模式,它允许在不破坏封装性的前提下捕获并外部化对象的内部状态,以便后续可以恢复到此状态。
模式参与者
适用场景对比表
| 场景 | 适用性 | 优势 | 注意事项 |
|---|---|---|---|
| 撤销/重做操作 | ⭐⭐⭐⭐⭐ | 完美支持多级撤销 | 内存占用需控制 |
| 事务性操作 | ⭐⭐⭐⭐ | 保证操作原子性 | 需要持久化支持 |
| 状态快照 | ⭐⭐⭐⭐ | 灵活的状态管理 | 状态序列化复杂度 |
| 工作流状态 | ⭐⭐⭐ | 支持流程回退 | 状态一致性维护 |
🏗️ CleanArchitecture中的备忘录模式设计
在CleanArchitecture架构中,备忘录模式需要遵循分层原则,确保各层职责清晰。
架构分层设计
核心领域对象设计
在Core层定义备忘录相关的领域模型:
// Core/ContributionAggregate/ContributionMemento.cs
namespace Clean.Architecture.Core.ContributionAggregate;
public class ContributionMemento : ValueObject
{
public string Name { get; private set; }
public ContributionStatus Status { get; private set; }
public DateTime SnapshotTime { get; private set; }
public string SerializedState { get; private set; }
public ContributionMemento(string name, ContributionStatus status, string serializedState)
{
Name = Guard.Against.NullOrEmpty(name, nameof(name));
Status = status;
SerializedState = Guard.Against.NullOrEmpty(serializedState, nameof(serializedState));
SnapshotTime = DateTime.UtcNow;
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Name;
yield return Status;
yield return SnapshotTime;
yield return SerializedState;
}
}
// Core/Interfaces/IMementoOriginator.cs
public interface IMementoOriginator
{
ContributionMemento CreateMemento();
void RestoreFromMemento(ContributionMemento memento);
}
领域对象实现备忘录接口
// Core/ContributionAggregate/Contribution.cs
public class Contribution : EntityBase, IAggregateRoot, IMementoOriginator
{
// 现有属性...
public string Name { get; private set; } = default!;
public ContributionStatus Status { get; private set; } = ContributionStatus.NotSet;
// 备忘录模式实现
public ContributionMemento CreateMemento()
{
var state = new
{
Name,
Status,
PhoneNumber = PhoneNumber?.ToString()
};
var serializedState = JsonSerializer.Serialize(state);
return new ContributionMemento(Name, Status, serializedState);
}
public void RestoreFromMemento(ContributionMemento memento)
{
var state = JsonSerializer.Deserialize<dynamic>(memento.SerializedState);
Name = state?.Name ?? string.Empty;
Status = state?.Status ?? ContributionStatus.NotSet;
// 根据需要恢复其他属性
}
}
🛠️ Use Cases层:备忘录操作用例
在Use Cases层实现具体的备忘录操作命令和查询:
// UseCases/Contributions/Memento/
namespace Clean.Architecture.UseCases.Contributions.Memento;
// 创建备忘录命令
public record CreateContributionMementoCommand(int ContributionId) : IRequest<ContributionMementoDto>;
public class CreateContributionMementoHandler : IRequestHandler<CreateContributionMementoCommand, ContributionMementoDto>
{
private readonly IRepository<Contribution> _repository;
private readonly IMementoRepository _mementoRepository;
public CreateContributionMementoHandler(IRepository<Contribution> repository, IMementoRepository mementoRepository)
{
_repository = repository;
_mementoRepository = mementoRepository;
}
public async Task<ContributionMementoDto> Handle(CreateContributionMementoCommand request, CancellationToken cancellationToken)
{
var contribution = await _repository.GetByIdAsync(request.ContributionId, cancellationToken);
if (contribution == null)
throw new NotFoundException(nameof(Contribution), request.ContributionId);
var memento = contribution.CreateMemento();
await _mementoRepository.AddAsync(memento, cancellationToken);
return new ContributionMementoDto(
memento.Name,
memento.Status.ToString(),
memento.SnapshotTime
);
}
}
// 恢复备忘录命令
public record RestoreContributionFromMementoCommand(int ContributionId, Guid MementoId) : IRequest;
public class RestoreContributionFromMementoHandler : IRequestHandler<RestoreContributionFromMementoCommand>
{
private readonly IRepository<Contribution> _repository;
private readonly IMementoRepository _mementoRepository;
public RestoreContributionFromMementoHandler(IRepository<Contribution> repository, IMementoRepository mementoRepository)
{
_repository = repository;
_mementoRepository = mementoRepository;
}
public async Task Handle(RestoreContributionFromMementoCommand request, CancellationToken cancellationToken)
{
var contribution = await _repository.GetByIdAsync(request.ContributionId, cancellationToken);
if (contribution == null)
throw new NotFoundException(nameof(Contribution), request.ContributionId);
var memento = await _mementoRepository.GetByIdAsync(request.MementoId, cancellationToken);
if (memento == null)
throw new NotFoundException(nameof(ContributionMemento), request.MementoId);
contribution.RestoreFromMemento(memento);
await _repository.UpdateAsync(contribution, cancellationToken);
}
}
📊 Infrastructure层:备忘录持久化
在Infrastructure层实现备忘录的存储和序列化:
// Infrastructure/Data/MementoRepository.cs
namespace Clean.Architecture.Infrastructure.Data;
public interface IMementoRepository
{
Task<ContributionMemento?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task<List<ContributionMemento>> GetByContributionIdAsync(int contributionId, CancellationToken cancellationToken = default);
Task AddAsync(ContributionMemento memento, CancellationToken cancellationToken = default);
Task DeleteAsync(Guid id, CancellationToken cancellationToken = default);
}
public class MementoRepository : IMementoRepository
{
private readonly AppDbContext _dbContext;
public MementoRepository(AppDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<ContributionMemento?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
return await _dbContext.Set<ContributionMemento>()
.FirstOrDefaultAsync(m => m.Id == id, cancellationToken);
}
public async Task<List<ContributionMemento>> GetByContributionIdAsync(int contributionId, CancellationToken cancellationToken = default)
{
return await _dbContext.Set<ContributionMemento>()
.Where(m => m.ContributionId == contributionId)
.OrderByDescending(m => m.SnapshotTime)
.ToListAsync(cancellationToken);
}
public async Task AddAsync(ContributionMemento memento, CancellationToken cancellationToken = default)
{
await _dbContext.Set<ContributionMemento>().AddAsync(memento, cancellationToken);
await _dbContext.SaveChangesAsync(cancellationToken);
}
public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default)
{
var memento = await GetByIdAsync(id, cancellationToken);
if (memento != null)
{
_dbContext.Set<ContributionMemento>().Remove(memento);
await _dbContext.SaveChangesAsync(cancellationToken);
}
}
}
🌐 Web层:RESTful API端点
在Web层提供备忘录操作的API端点:
// Web/Contributions/Memento/
namespace Clean.Architecture.Web.Contributions.Memento;
public static class MementoEndpoints
{
public static void MapContributionMementoEndpoints(this IEndpointRouteBuilder routes)
{
var group = routes.MapGroup("/api/contributions/{contributionId}/mementos");
group.MapGet("", async (int contributionId, ISender mediator, CancellationToken cancellationToken) =>
{
var query = new GetContributionMementosQuery(contributionId);
var result = await mediator.Send(query, cancellationToken);
return Results.Ok(result);
}).WithName("GetContributionMementos");
group.MapPost("", async (int contributionId, ISender mediator, CancellationToken cancellationToken) =>
{
var command = new CreateContributionMementoCommand(contributionId);
var result = await mediator.Send(command, cancellationToken);
return Results.Created($"/api/contributions/{contributionId}/mementos/{result.Id}", result);
}).WithName("CreateContributionMemento");
group.MapPost("{mementoId}/restore", async (int contributionId, Guid mementoId, ISender mediator, CancellationToken cancellationToken) =>
{
var command = new RestoreContributionFromMementoCommand(contributionId, mementoId);
await mediator.Send(command, cancellationToken);
return Results.NoContent();
}).WithName("RestoreContributionFromMemento");
}
}
⚡ 性能优化策略
内存管理优化
// 使用轻量级状态序列化
public class OptimizedMementoSerializer
{
public string SerializeState(Contribution contribution)
{
// 使用MessagePack或Protobuf进行高效序列化
using var stream = new MemoryStream();
MessagePackSerializer.Serialize(stream, new
{
contribution.Name,
contribution.Status,
// 只序列化必要字段
});
return Convert.ToBase64String(stream.ToArray());
}
public dynamic DeserializeState(string serializedState)
{
var bytes = Convert.FromBase64String(serializedState);
using var stream = new MemoryStream(bytes);
return MessagePackSerializer.Deserialize<dynamic>(stream);
}
}
数据库优化策略
| 策略 | 实现方式 | 优势 | 适用场景 |
|---|---|---|---|
| 分表存储 | 按时间分表 | 查询性能提升 | 历史数据量大 |
| 压缩存储 | GZip压缩 | 存储空间节省 | 状态数据较大 |
| 增量快照 | 只存储变化 | 存储效率高 | 频繁状态变更 |
| 缓存策略 | Redis缓存 | 读取速度快 | 高频访问 |
🔄 与领域事件集成
备忘录模式可以与领域事件完美结合,实现更强大的状态管理:
// Core/ContributionAggregate/Events/ContributionMementoCreatedEvent.cs
public class ContributionMementoCreatedEvent : DomainEventBase
{
public int ContributionId { get; }
public Guid MementoId { get; }
public DateTime SnapshotTime { get; }
public ContributionMementoCreatedEvent(int contributionId, Guid mementoId, DateTime snapshotTime)
{
ContributionId = contributionId;
MementoId = mementoId;
SnapshotTime = snapshotTime;
}
}
// 在创建备忘录时发布事件
public ContributionMemento CreateMemento()
{
var memento = // 创建备忘录逻辑
AddDomainEvent(new ContributionMementoCreatedEvent(Id, memento.Id, memento.SnapshotTime));
return memento;
}
🧪 单元测试示例
// UnitTests/Core/ContributionAggregate/ContributionMementoTests.cs
public class ContributionMementoTests
{
[Fact]
public void CreateMemento_ShouldCaptureCurrentState()
{
// Arrange
var contribution = new Contribution("Test Contribution");
contribution.SetPhoneNumber("123-456-7890");
// Act
var memento = contribution.CreateMemento();
// Assert
memento.Name.Should().Be("Test Contribution");
memento.SnapshotTime.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
}
[Fact]
public void RestoreFromMemento_ShouldRecoverPreviousState()
{
// Arrange
var original = new Contribution("Original");
original.SetPhoneNumber("original-phone");
var memento = original.CreateMemento();
var restored = new Contribution("New");
restored.SetPhoneNumber("new-phone");
// Act
restored.RestoreFromMemento(memento);
// Assert
restored.Name.Should().Be("Original");
restored.PhoneNumber.Number.Should().Be("original-phone");
}
}
🚀 部署与运维考虑
监控指标
配置建议
{
"MementoSettings": {
"MaxMementosPerEntity": 50,
"RetentionPeriodDays": 30,
"CompressionEnabled": true,
"CacheEnabled": true,
"CacheDurationMinutes": 5
}
}
📈 最佳实践总结
- 适度使用:只在真正需要状态恢复功能的领域对象上实现备忘录模式
- 性能考量:对大对象使用增量快照和压缩策略
- 安全考虑:对敏感数据进行加密存储
- 清理策略:实现自动清理过期备忘录的机制
- 监控告警:设置存储空间和性能监控告警
🎯 下一步行动
- 在您的Contribution实体中实现IMementoOriginator接口
- 配置MementoRepository和相关服务
- 添加备忘录相关的API端点
- 实现自动清理任务
- 添加监控和日志记录
备忘录模式在CleanArchitecture中的实现不仅提供了强大的状态管理能力,更重要的是保持了架构的整洁性和可维护性。通过遵循本文的指导,您可以在项目中优雅地实现撤销/重做、事务回滚等复杂功能。
点赞/收藏/关注三连,获取更多CleanArchitecture最佳实践!下期我们将探讨「CleanArchitecture中的策略模式与插件架构」。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



