探索清洁clean架构:实用指南

想象一下,您的系统需要支持新的数据库或不同的用户界面。使用 Clean Architecture,此类更改变得更容易处理,因为核心业务逻辑不会受到外部依赖项的影响。正如 Bob 大叔所强调的那样,应用程序的中心应该是用例和业务逻辑,而不是框架或数据库。

为什么选择清洁架构?
清洁架构有助于缓解几个常见的架构问题:

  • 早期承诺:传统架构通常会迫使团队在项目开始时就做出关键决策,因为那时他们对问题领域的了解很少。清洁架构鼓励将有关框架、数据库和其他细节的决策推迟到必要时再做,从而随着需求的发展,设计可以随时更改。
  • 僵化且难以改变的系统:如果没有清晰的结构,新的需求通常需要“黑客”解决方案或昂贵的重新设计。通过将业务规则与实施细节分离,清晰架构使系统更易于适应和扩展。
  • 以框架为中心的设计:框架应该是工具,而不是架构本身。它们可以发展并引入重大变化,但如果您的系统与框架无关,则不会受到严重影响。Bob 大叔强调,框架是细节,应该放在边缘。
  • 数据库优先思维:许多系统都是围绕数据库构建的,将所有内容都转化为 CRUD 操作。Clean Architecture 将数据库视为另一个数据提供者,确保业务逻辑与数据库无关。
  • 分散的业务逻辑:当业务规则分散在多个层上时,理解或修改行为会变得困难。清晰架构将业务逻辑集中在用例内,使其易于定位和维护。
  • 测试缓慢而脆弱:将业务逻辑与 UI 或数据库耦合会使测试缓慢而脆弱。清洁架构提倡解耦,从而实现专注于核心逻辑的快速、可靠的单元测试。

清洁架构的关键概念
1. 分离关注点以实现灵活性
清晰架构将职责组织到不同的层中,从而减少依赖性并简化维护。每层都有特定的角色,从而产生更可预测且更有条理的代码库。

2.以领域为中心的设计
领域层是系统的核心,封装了业务逻辑和必要实体。它独立于其他层,紧密贴合业务需求并简化单元测试。

3. 用例和应用逻辑
用例是特定于应用程序的业务规则,用于协调实体之间的交互。它们处理输入和输出,但不知道数据源或呈现细节。

  • 请求和响应模型:使用简单的数据结构将用例与框架分离,保持核心逻辑的集中和可测试。
  • CQRS(命令查询职责分离):CQRS 模式将数据读写操作分离,优化性能并使代码更清晰。这种方法可确保应用层处理业务逻辑而无需担心基础设施。

4. 基础设施即插件
基础设施层管理外部集成,例如数据库和消息传递系统,隐藏应用程序其余部分的实现细节。将基础设施视为插件可以更轻松地替换或修改技术,而不会影响业务逻辑。

  • 六边形架构:也称为端口和适配器,这种模式强调核心和外部系统之间的明确分离,从而增强灵活性。

5. 表示层:用户界面
表示层处理用户交互,通常通过 RESTful API 或 gRPC 进行。它将业务逻辑委托给应用层,仅关注输入和输出。

6.依赖注入
依赖注入对于维护架构完整性至关重要。它控制各层之间的依赖关系,实现灵活性并简化测试。


让我们通过实际的例子来分解每一层。

领域层
领域层定义业务实体和核心规则。例如,一个Webinar实体可能如下所示:

public class Webinar
{
    public Guid Id { get; private set; }
    public string Name { get; private set; }
    public DateTime ScheduledOn { get; private set; }

    public Webinar(string name, DateTime scheduledOn)
    {
        Id = Guid.NewGuid(); // Generates a unique identifier for the webinar
        Name = name;
        ScheduledOn = scheduledOn;
    }

    public void Reschedule(DateTime newDate)
    {
        ScheduledOn = newDate;
    }
}


实体是自包含的,并根据业务需求而不是外部系统约束进行发展。我们还定义了存储库接口和自定义异常:

public interface IWebinarRepository
{
    Task<Webinar?> GetById(Guid id, CancellationToken cancellationToken);

    Task Add(Webinar webinar, CancellationToken cancellationToken);
}


public class WebinarNotFoundException : Exception
{
    public WebinarNotFoundException(Guid webinarId)
        : base($"Webinar with ID {webinarId} was not found.") { }
}


应用层
应用层管理用例并实现CQRS以分离命令和查询,确保效率。

CreateWebinarCommand命令:

public class CreateWebinarCommand : IRequest<Guid>
{
    public string Name { get; set; }
    public DateTime ScheduledOn { get; set; }
}


    
CreateWebinarCommandHandler处理程序:

public class CreateWebinarCommandHandler : IRequestHandler<CreateWebinarCommand, Guid>
{
    private readonly IWebinarRepository _repository;

    public CreateWebinarCommandHandler(IWebinarRepository repository)
    {
        _repository = repository;
    }


    public async Task<Guid> Handle(CreateWebinarCommand command, CancellationToken cancellationToken)
    {
        var webinar = new Webinar(command.Name, command.ScheduledOn);
        await _repository.Add(webinar, cancellationToken);
        return webinar.Id;
    }
}


GetWebinarByIdQuery查询:

public class GetWebinarByIdQuery : IRequest<Webinar?>
{
    public Guid Id { get; set; }
}


GetWebinarByIdQueryHandler处理程序:

public class GetWebinarByIdQueryHandler : IRequestHandler<GetWebinarByIdQuery, Webinar?>
{
    private readonly IWebinarRepository _repository;

    public GetWebinarByIdQueryHandler(IWebinarRepository repository)
    {
        _repository = repository;
    }

    public async Task<Webinar?> Handle(GetWebinarByIdQuery request, CancellationToken cancellationToken)
    {
        var webinar = await _repository.GetById(request.Id, cancellationToken);
        if (webinar is null)
            throw new WebinarNotFoundException(request.Id);

        return webinar;
    }
}


这种结构使用例保持隔离并且易于测试。

基础设施层
基础设施层处理外部系统集成,如数据库访问:

public class WebinarRepository : IWebinarRepository
{
    private readonly AppDbContext _dbContext;

    public WebinarRepository(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<Webinar?> GetById(Guid id, CancellationToken cancellationToken)
    {
        return await _dbContext.Webinars.FindAsync(id, cancellationToken);
    }

    public async Task Add(Webinar webinar, CancellationToken cancellationToken)
    {
        await _dbContext.Webinars.AddAsync(webinar, cancellationToken);
        await _dbContext.SaveChangesAsync(cancellationToken);
    }
}


通过封装这种逻辑,系统可以灵活地适应技术变化。

表示层
表示层提供用于用户交互的API:

[ ApiController ] 
[ Route( "api/[controller]" ) ] 
public  class  WebinarsController : ControllerBase
 { 
    private  readonly IMediator _mediator; 

    public  WebinarsController ( IMediator mediator )
     { 
        _mediator = mediator; 
    } 

    [ HttpPost ] 
    public  async Task<IActionResult> CreateWebinar ( [FromBody] CreateWebinarCommand command )
     { 
        var webinarId = await _mediator.Send(command); 
        return CreatedAtAction( nameof (GetWebinar), new { id = webinarId }, webinarId); 
    } 

    [ HttpGet(
"{id}" ) ] 
    public  async Task<IActionResult> GetWebinar ( Guid id )
     { 
        var query = new GetWebinarByIdQuery { Id = id }; 
        var webinar = await _mediator.Send(query); 
        return Ok(webinar); 
    } 
}

通过将逻辑卸载到应用层,该层专注于处理请求和响应。

使用中间件进行集中错误处理
集中错误处理可提高用户体验和安全性:

public class ExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;

    public ExceptionHandlingMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch(WebinarNotFoundException ex)
        {
            context.Response.StatusCode = StatusCodes.Status404NotFound;
            await context.Response.WriteAsJsonAsync(new { Error = ex.Message });
        }
        catch (Exception ex)
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            await context.Response.WriteAsJsonAsync(new { Error = ex.Message });
        }
    }
}

依赖注册Program.cs
配置依赖项Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
                .AddApplicationPart(typeof(WebinarsController).Assembly);

builder.Services.AddScoped<IWebinarRepository, WebinarRepository>();

builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(typeof(CreateWebinarCommandHandler).Assembly));

builder.Services.AddDbContext<AppDbContext>(options =>
           options.UseInMemoryDatabase("webinarsDb"));

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

app.UseMiddleware<ExceptionHandlingMiddleware>();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();


清洁架构的好处和权衡

清洁架构的好处和权衡
好处:

  • 促进有效的测试策略。
  • 与框架无关的设计最大限度地减少了外部依赖。
  • 业务逻辑的明确分离增强了理解和修改。
  • 支持增量部署和持续集成。
权衡:
  • 复杂性:引入多个边界会增加开销。请明智地使用边界。
  • 代码重复:实体的不同表示可能看似冗余,但却能促进解耦。

结论
清晰架构提供可持续的模块化软件结构。通过维护定义明确的层次,系统变得更易于维护且不易出错,可满足不断变化的需求和技术。