🛠️ Explorando a Arquitetura Limpa: Um Guia Prático 📖
A Arquitetura Limpa é uma abordagem de design de software que separa as responsabilidades do sistema em camadas bem definidas. Essa estrutura modular simplifica a manutenção e escalabilidade, promovendo a separação de preocupações e facilitando a compreensão e modificações no código. Vamos explorar como cada camada funciona e por que ela é importante.
Imagine um cenário em que seu sistema precisa suportar um novo banco de dados ou uma interface de usuário diferente. Com a Arquitetura Limpa, essas mudanças se tornam mais fáceis de lidar, pois a lógica central de negócios permanece inalterada por dependências externas. Como enfatiza o Uncle Bob, o centro da sua aplicação deve ser composto pelos casos de uso e a lógica de negócios, não frameworks ou bancos de dados.
Por Que Escolher a Arquitetura Limpa?
A Arquitetura Limpa ajuda a mitigar vários problemas arquiteturais comuns:
- Compromissos Precoces: Arquiteturas tradicionais muitas vezes forçam as equipes a tomar decisões importantes no início de um projeto, quando o entendimento do domínio do problema ainda é mínimo. A Arquitetura Limpa incentiva o adiamento de decisões sobre frameworks, bancos de dados e outros detalhes até que sejam necessários, mantendo o design aberto a mudanças conforme os requisitos evoluem.
- Sistemas Rígidos e Difíceis de Alterar: Sem uma estrutura limpa, novos requisitos geralmente exigem uma solução "improvisada" ou uma reformulação custosa. Ao desacoplar as regras de negócio dos detalhes de implementação, a Arquitetura Limpa torna o sistema mais fácil de adaptar e expandir.
- Design Centrado em Frameworks: Frameworks devem ser ferramentas, não a própria arquitetura. Eles podem evoluir e introduzir mudanças incompatíveis, mas se o seu sistema for independente de framework, ele não será afetado de forma tão grave. Uncle Bob enfatiza que frameworks são detalhes e devem ser mantidos na periferia.
- Pensamento Focado em Banco de Dados: Muitos sistemas são construídos em torno do banco de dados, transformando tudo em operações CRUD. A Arquitetura Limpa trata o banco de dados como apenas mais um fornecedor de dados, garantindo que a lógica de negócios permaneça independente do banco de dados.
- Lógica de Negócios Dispersa: Quando as regras de negócio estão espalhadas por várias camadas, entender ou modificar o comportamento se torna difícil. A Arquitetura Limpa centraliza a lógica de negócios dentro dos casos de uso, tornando-a fácil de localizar e manter.
- Testes Lentos e Frágeis: O acoplamento da lógica de negócios com a interface de usuário ou o banco de dados pode tornar os testes lentos e frágeis. A Arquitetura Limpa promove o desacoplamento, permitindo testes de unidade rápidos e confiáveis que se concentram na lógica central.
Conceitos-Chave da Arquitetura Limpa
1. Separação de Preocupações para Flexibilidade
A Arquitetura Limpa organiza responsabilidades em camadas distintas, reduzindo dependências e facilitando a manutenção. Cada camada tem um papel específico, resultando em uma base de código mais previsível e organizada.
2. Design Centrado no Domínio
A camada de domínio é o núcleo do sistema, encapsulando a lógica de negócios e as entidades essenciais. Ela é independente das outras camadas, aderindo estritamente aos requisitos de negócios e simplificando os testes de unidade.
3. Casos de Uso e Lógica de Aplicação
Os casos de uso são regras de negócios específicas da aplicação que coordenam interações entre entidades. Eles lidam com entrada e saída sem ter conhecimento de fontes de dados ou detalhes de apresentação.
- Modelos de Requisição e Resposta: Use estruturas de dados simples para desacoplar casos de uso de frameworks, mantendo a lógica central focada e testável.
- CQRS (Separação de Responsabilidade de Comando e Consulta): O padrão CQRS separa as operações de leitura e escrita de dados, otimizando o desempenho e tornando o código mais claro. Essa abordagem garante que a camada de aplicação lide com a lógica de negócios sem preocupações com infraestrutura.
4. Infraestrutura como um Plugin
A camada de infraestrutura gerencia integrações externas, como bancos de dados e sistemas de mensagens, ocultando detalhes de implementação do restante da aplicação. Tratar a infraestrutura como plugins facilita a substituição ou modificação da tecnologia sem impactar a lógica de negócios.
- Arquitetura Hexagonal: Também conhecida como Ports and Adapters, esse padrão enfatiza uma separação limpa entre o núcleo e sistemas externos, aumentando a flexibilidade.
5. Camada de Apresentação: A Interface do Usuário
A camada de apresentação lida com a interação do usuário, frequentemente através de APIs RESTful ou gRPC. Ela delega a lógica de negócios para a camada de aplicação, focando apenas na entrada e saída.
6. Injeção de Dependência
A injeção de dependência é crucial para manter a integridade da arquitetura. Ela controla as dependências entre as camadas, permitindo flexibilidade e simplificando os testes.
Exemplo Prático: Camadas e Funcionalidade
Vamos detalhar cada camada com exemplos práticos.
Camada de Domínio
A Camada de Domínio define entidades de negócios e regras principais. Por exemplo, uma entidade Webinar
pode ser assim:
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(); // Gera um identificador único para o webinar
Name = name;
ScheduledOn = scheduledOn;
}
public void Reschedule(DateTime newDate)
{
ScheduledOn = newDate;
}
}
As entidades são autocontidas e evoluem com base nas necessidades de negócios, não em restrições de sistemas externos. Também definimos interfaces de repositório e exceções personalizadas:
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 com ID {webinarId} não foi encontrado.") { }
}
Camada de Aplicação
A Camada de Aplicação gerencia os casos de uso e implementa CQRS para separar comandos e consultas, garantindo eficiência.
Comando CreateWebinarCommand
:
public class CreateWebinarCommand : IRequest<Guid>
{
public string Name { get; set; }
public DateTime ScheduledOn { get; set; }
}
Manipulador 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;
}
}
Consulta GetWebinarByIdQuery
:
public class GetWebinarByIdQuery : IRequest<Webinar?>
{
public Guid Id { get; set; }
}
Manipulador 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;
}
}
Essa estrutura mantém os casos de uso isolados e facilmente testáveis.
Camada de Infraestrutura
A Camada de Infraestrutura lida com integrações externas, como acesso ao banco de dados:
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);
}
}
Encapsulando essa lógica, o sistema permanece flexível a mudanças tecnológicas.
Camada de Apresentação
A Camada de Apresentação fornece APIs para a interação do usuário:
[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);
}
}
Ao delegar a lógica para a camada de aplicação, essa camada se concentra em lidar com requisições e respostas.
Manipulação Centralizada de Erros com Middleware
A manipulação centralizada de erros melhora a experiência do usuário e a segurança:
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 });
}
}
}
Registro de Dependências no Program.cs
Configurando dependências no 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();
Benefícios e Compromissos da Arquitetura Limpa
- Benefícios:
- Facilita estratégias de teste eficazes.
- Design independente de framework minimiza dependências externas.
- Separação clara da lógica de negócios melhora a compreensão e modificação.
- Suporta implantações incrementais e integração contínua.
- Compromissos:
- Complexidade: Introduzir múltiplas fronteiras pode adicionar sobrecarga. Use-as com sabedoria.
- Duplicação de Código: Representações diferentes de entidades podem parecer redundantes, mas promovem o desacoplamento.
Conclusão
A Arquitetura Limpa oferece uma estrutura modular e sustentável para software. Ao manter camadas bem definidas, os sistemas se tornam mais fáceis de manter e menos propensos a erros, prontos para requisitos e tecnologias em evolução.