Este documento mapeia cada princípio aplicado no projeto ao trecho de código correspondente. Os trechos destacados em negrito identificam o ponto exato de aplicação.
Objetivo: Ocultar detalhes de implementação e expor apenas contratos essenciais.
Trecho 1 — Entity (classe abstrata)
Arquivo: src/CookieStore.Domain/SharedKernel/Entity.cs
public abstract class Entity // ← ABSTRAÇÃO: não pode ser instanciada diretamente
{
public Guid Id { get; protected set; }
protected Entity(Guid id) { Id = id; }
public virtual string Describe() => $"{GetType().Name} [Id={Id}]"; // ← contrato extensível
// ...
}A palavra-chave abstract impede que Entity seja instanciada diretamente.
Apenas subclasses concretas (Sabor, Venda, Lote) são criadas — o detalhe de como a
identidade é gerenciada e como a igualdade funciona fica oculto dos chamadores.
Trecho 2 — ValueObject (classe abstrata)
Arquivo: src/CookieStore.Domain/SharedKernel/ValueObject.cs
public abstract class ValueObject // ← ABSTRAÇÃO: contrato de igualdade estrutural
{
protected abstract IEnumerable<object> GetEqualityComponents(); // ← contrato obrigatório
// ...
}O método abstract GetEqualityComponents() declara o contrato sem implementá-lo.
Cada Value Object decide quais componentes definem sua igualdade — os chamadores usam
Equals e == sem conhecer os campos internos.
Trecho 3 — Interfaces de Repository
Arquivo: src/CookieStore.Domain/SharedKernel/ISaborRepository.cs
public interface ISaborRepository
{
Task<Sabor?> GetByIdAsync(SaborId id);
Task<IEnumerable<Sabor>> GetAllActiveAsync();
Task AddAsync(Sabor sabor);
Task UpdateAsync(Sabor sabor);
}A interface ISaborRepository abstrai completamente a tecnologia de persistência (EF Core,
SQL Server). O Domain e o Application nunca sabem que existe um banco de dados.
Objetivo: Proteger o estado interno e expor apenas o que é necessário.
Trecho 1 — Private setters nos Aggregates
Arquivo: src/CookieStore.Domain/SharedKernel/Sabor.cs
public class Sabor : Entity
{
public string Name { get; private set; } // ← ENCAPSULAMENTO: setter privado
public PrecoTabela CatalogPrice { get; private set; } // ← setter privado
public bool IsActive { get; private set; } // ← setter privado
public void UpdatePrice(PrecoTabela newPrice) // ← mutação apenas via método
{
CatalogPrice = newPrice;
}
public void Deactivate() // ← mutação apenas via método
{
IsActive = false;
}
}Nenhuma propriedade tem setter público. O estado do Sabor só muda via métodos de domínio
nomeados, garantindo que as invariantes sejam sempre verificadas.
Trecho 2 — Private constructor nos Value Objects
Arquivo: src/CookieStore.Domain/SharedKernel/PrecoTabela.cs
public sealed class PrecoTabela : ValueObject
{
public decimal Value { get; } // ← sem setter — imutável
private PrecoTabela(decimal value) => Value = value; // ← ENCAPSULAMENTO: ctor privado
public static PrecoTabela Create(decimal value) // ← único ponto de criação
{
if (value <= 0) throw new DomainException("Catalog price must be positive.");
return new PrecoTabela(value);
}
}O construtor privado garante que PrecoTabela só pode ser criado via Create(), onde a
validação é obrigatória. É impossível construir um PrecoTabela inválido.
Trecho 3 — Internal constructor no Aggregate Root Venda
Arquivo: src/CookieStore.Domain/Vendas/Venda.cs
internal Venda(Guid id, SaborId saborId, Quantidade quantity, decimal unitPrice, DateTime soldAt)
: base(id)
// ← ENCAPSULAMENTO: internal — apenas VendaFactory (mesmo assembly) pode criar VendaO modificador internal restringe a criação de Venda à VendaFactory, que está no
mesmo assembly (CookieStore.Domain). Código da camada Application não pode usar new Venda(...).
Objetivo: Criar hierarquias de classes que compartilham comportamento comum.
Trecho 1 — Aggregates herdam de Entity
// src/CookieStore.Domain/SharedKernel/Sabor.cs
public class Sabor : Entity { ... }
// src/CookieStore.Domain/Vendas/Venda.cs
public class Venda : Entity { ... }
// src/CookieStore.Domain/Producao/Lote.cs
public class Lote : Entity { ... }Os três Aggregate Roots herdam de Entity, recebendo gratuitamente:
Guid Idcomprotected setEquals()baseado em identidadeGetHashCode()e operadores==/!=
Trecho 2 — Value Objects herdam de ValueObject
// src/CookieStore.Domain/SharedKernel/PrecoTabela.cs
public sealed class PrecoTabela : ValueObject { ... }
// src/CookieStore.Domain/Vendas/Quantidade.cs
public sealed class Quantidade : ValueObject { ... }
// src/CookieStore.Domain/Producao/CustoTotal.cs
public sealed class CustoTotal : ValueObject { ... }
// src/CookieStore.Domain/Producao/Rendimento.cs
public sealed class Rendimento : ValueObject { ... }Os quatro Value Objects herdam de ValueObject, recebendo a implementação de Equals,
GetHashCode e os operadores de comparação sem duplicação de código.
Trecho 3 — SaborAclAdapter implementa ISaborAcl
Arquivo: src/CookieStore.Application/Vendas/SaborAclAdapter.cs
public class SaborAclAdapter : ISaborAcl // ← HERANÇA via interface
{
public async Task<SaborSnapshot?> GetSnapshotAsync(SaborId id) { ... }
}Objetivo: Tratar objetos de tipos diferentes de forma uniforme através de uma interface comum.
Trecho 1 — Describe() virtual com override
Arquivo: src/CookieStore.Domain/SharedKernel/Entity.cs e subclasses
// Entity — método virtual (comportamento padrão)
public virtual string Describe() => $"{GetType().Name} [Id={Id}]";
// Sabor — override com comportamento específico
public override string Describe() => $"Sabor [{Name}] Id={Id}";
// Venda — override com comportamento específico
public override string Describe() => $"Venda [SaborId={SaborId}] SoldAt={SoldAt:yyyy-MM-dd}";
// Lote — override com comportamento específico
public override string Describe() => $"Lote [SaborId={SaborId}] ProducedAt={ProducedAt:yyyy-MM-dd}";A palavra-chave virtual em Entity e override nas subclasses demonstram polimorfismo
por sobrescrita. O mesmo método Describe() produz resultados diferentes dependendo do tipo
concreto do objeto em tempo de execução.
Trecho 2 — GetEqualityComponents() abstract com override
Arquivo: src/CookieStore.Domain/SharedKernel/ValueObject.cs e VOs
// ValueObject — método abstrato (POLIMORFISMO: força override em cada subclasse)
protected abstract IEnumerable<object> GetEqualityComponents();
// PrecoTabela — override que define a igualdade pelo Value decimal
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Value;
}
// Quantidade — override que define a igualdade pelo Value int
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Value;
}O método abstract em ValueObject força cada subclasse a declarar override.
O método Equals da classe base chama GetEqualityComponents() — que em tempo de execução
despacha para a versão correta de cada VO. Isso é polimorfismo via dispatch virtual.
Trecho 3 — Polimorfismo via interface (DI)
Arquivo: src/CookieStore.Application/Vendas/VendaAppService.cs
public class VendaAppService
{
private readonly ISaborAcl _acl; // ← POLIMORFISMO: referência à interface
private readonly IVendaRepository _vendas;
// Em produção: SaborAclAdapter + EfVendaRepository
// Em testes: NSubstitute.For<ISaborAcl>() + NSubstitute.For<IVendaRepository>()
// O VendaAppService não sabe qual implementação recebe
}Objetivo: Cada classe deve ter exatamente uma razão para mudar.
Aplicação 1 — RelatorioService
Arquivo: src/CookieStore.Domain/Vendas/RelatorioService.cs
A classe tem uma única responsabilidade: calcular lucro e resumo de vendas por período. Não persiste, não valida entrada HTTP, não mapeia DTOs.
public class RelatorioService : IRelatorioService
{
// Única responsabilidade: calcular relatórios cruzando Vendas e Producao
public async Task<IEnumerable<LucroPorSabor>> GetProfitBySaborAsync(DateTime from, DateTime to) { ... }
public async Task<ResumoVendas> GetSummaryAsync(DateTime from, DateTime to) { ... }
}Aplicação 2 — SaborAppService
Arquivo: src/CookieStore.Application/Sabores/SaborAppService.cs
A classe tem uma única responsabilidade: orquestrar os casos de uso do Sabor.
Não contém lógica de negócio — delega ao domínio.
Objetivo: Classes abertas para extensão, fechadas para modificação.
Aplicação — Novos sabores sem modificar o domínio
Arquivo: src/CookieStore.Domain/SharedKernel/Entity.cs
public abstract class Entity
{
public virtual string Describe() => $"{GetType().Name} [Id={Id}]"; // ← ABERTA para extensão
}Adicionar um novo Aggregate Root (ex: Encomenda) requer apenas criar uma nova subclasse
de Entity com override Describe(). A classe Entity nunca precisa ser modificada.
O mesmo vale para ValueObject: novos VOs são criados herdando da base — sem alterar a base.
Objetivo: Implementações devem ser substituíveis por seus contratos sem quebrar o sistema.
Aplicação — Repositories
Arquivo: src/CookieStore.Infrastructure/Repositories/SaborRepository.cs
public class SaborRepository : ISaborRepository
{
public async Task<Sabor?> GetByIdAsync(SaborId id) => ...; // implementa contrato
public async Task<IEnumerable<Sabor>> GetAllActiveAsync() => ...; // implementa contrato
public async Task AddAsync(Sabor sabor) { ... } // implementa contrato
public async Task UpdateAsync(Sabor sabor) { ... } // implementa contrato
}SaborRepository implementa todos os métodos de ISaborRepository sem exceções ou
comportamentos inesperados. Em testes, NSubstitute.For<ISaborRepository>() substitui
SaborRepository sem que nenhum código de produção perceba a diferença.
Objetivo: Interfaces não devem forçar implementadores a depender de métodos que não usam.
Aplicação — Três interfaces separadas de Repository
// ISaborRepository — apenas operações de Sabor
public interface ISaborRepository
{
Task<Sabor?> GetByIdAsync(SaborId id);
Task<IEnumerable<Sabor>> GetAllActiveAsync();
Task AddAsync(Sabor sabor);
Task UpdateAsync(Sabor sabor);
}
// IVendaRepository — apenas operações de Venda (sem UpdateAsync — Venda é imutável)
public interface IVendaRepository
{
Task<IEnumerable<Venda>> GetByPeriodAsync(DateTime from, DateTime to);
Task<IEnumerable<Venda>> GetBySaborAsync(SaborId saborId);
Task AddAsync(Venda venda);
}
// ILoteRepository — apenas operações de Lote (sem UpdateAsync — Lote é imutável)
public interface ILoteRepository
{
Task<IEnumerable<Lote>> GetByPeriodAsync(DateTime from, DateTime to);
Task<IEnumerable<Lote>> GetBySaborAsync(SaborId saborId);
Task AddAsync(Lote lote);
}Cada interface contém apenas os métodos necessários para o aggregate correspondente. Nenhuma implementação é forçada a implementar métodos que não fazem sentido para ela.
Objetivo: Módulos de alto nível devem depender de abstrações, não de concreções.
Aplicação 1 — VendaAppService depende de abstrações
Arquivo: src/CookieStore.Application/Vendas/VendaAppService.cs
public class VendaAppService
{
private readonly ISaborAcl _acl; // ← abstração (DIP)
private readonly IVendaRepository _vendas; // ← abstração (DIP)
private readonly VendaFactory _factory; // ← objeto de domínio puro (sem infra)
public VendaAppService(ISaborAcl acl, IVendaRepository vendas, VendaFactory factory)
{
_acl = acl;
_vendas = vendas;
_factory = factory;
}
}VendaAppService nunca referencia SaborRepository, VendaRepository ou qualquer
classe do EF Core. Isso permite trocar a persistência sem tocar na camada Application.
Aplicação 2 — RelatorioAppService depende de IRelatorioService
Arquivo: src/CookieStore.Application/Relatorio/RelatorioAppService.cs
public class RelatorioAppService
{
private readonly IRelatorioService _relatorio; // ← abstração (DIP)
// nunca depende de RelatorioService diretamente
}Objetivo: Minimizar as dependências entre classes para reduzir o impacto de mudanças.
Aplicação — VendaFactory desacoplada de Sabor
Arquivo: src/CookieStore.Domain/Vendas/VendaFactory.cs
public class VendaFactory
{
public Venda Create(SaborSnapshot snapshot, int quantity) // ← recebe SaborSnapshot, não Sabor
{
var qty = Quantidade.Create(quantity);
return new Venda(Guid.NewGuid(), snapshot.Id, qty, snapshot.CurrentPrice, DateTime.UtcNow);
}
}Antes da implementação do ACL, VendaFactory dependia de Sabor (SharedKernel).
Agora depende apenas de SaborSnapshot (Vendas BC) — o acoplamento entre BCs foi eliminado.
Objetivo: Cada classe agrupa apenas responsabilidades fortemente relacionadas.
Aplicação — Lote calcula CostPerUnit internamente
Arquivo: src/CookieStore.Domain/Producao/Lote.cs
public class Lote : Entity
{
public Rendimento Yield { get; private set; }
public CustoTotal TotalCost { get; private set; }
public decimal CostPerUnit => TotalCost.Value / Yield.Value; // ← calculado internamente
}CostPerUnit é calculado por Lote porque Lote possui os dois operandos (TotalCost e
Yield). Mover esse cálculo para fora quebraria a coesão — um serviço externo precisaria
conhecer a estrutura interna do Lote.
Objetivo: Atribuir responsabilidades à classe que possui as informações necessárias.
Aplicação 1 — Venda calcula Total
Arquivo: src/CookieStore.Domain/Vendas/Venda.cs
public decimal Total => UnitPrice * Quantity.Value; // ← Venda tem ambos os operandosVenda é a especialista em informação para calcular o total, pois possui UnitPrice e
Quantity. Nenhuma outra classe precisa conhecer esses detalhes para obter o total.
Aplicação 2 — RelatorioService como especialista cross-BC
Arquivo: src/CookieStore.Domain/Vendas/RelatorioService.cs
public class RelatorioService : IRelatorioService
{
private readonly IVendaRepository _vendaRepository; // acesso a receitas
private readonly ILoteRepository _loteRepository; // acesso a custos
// ← única classe com acesso a ambos os BCs — especialista legítima para calcular lucro
}Objetivo: Atribuir a criação de objetos à classe que os agrega, contém ou usa de perto.
Aplicação — VendaFactory cria Venda
Arquivo: src/CookieStore.Domain/Vendas/VendaFactory.cs
public class VendaFactory
{
public Venda Create(SaborSnapshot snapshot, int quantity) // ← CREATOR: cria Venda
{
var qty = Quantidade.Create(quantity);
return new Venda(Guid.NewGuid(), snapshot.Id, qty, snapshot.CurrentPrice, DateTime.UtcNow);
}
}VendaFactory é a Creator de Venda porque:
- Tem o conhecimento de criação (captura de snapshot do preço)
- O construtor de
Vendaéinternal— só a factory pode instanciá-la - Encapsula a lógica de criação complexa (geração de ID, timestamp, snapshot)
O mesmo padrão se aplica a LoteFactory para Lote.
| Requisito | Princípio | Arquivo principal |
|---|---|---|
| Encapsulamento | OOP | Sabor.cs, PrecoTabela.cs, Venda.cs |
| Abstração | OOP | Entity.cs, ValueObject.cs, ISaborRepository.cs |
| Herança | OOP | Sabor/Venda/Lote : Entity, PrecoTabela/.../Rendimento : ValueObject |
| Polimorfismo | OOP | Describe() virtual/override, GetEqualityComponents() abstract/override |
| SRP | SOLID-S | RelatorioService.cs, SaborAppService.cs |
| OCP | SOLID-O | Entity.cs (virtual Describe()), ValueObject.cs |
| LSP | SOLID-L | SaborRepository.cs, VendaRepository.cs, LoteRepository.cs |
| ISP | SOLID-I | ISaborRepository.cs, IVendaRepository.cs, ILoteRepository.cs |
| DIP | SOLID-D | VendaAppService.cs, RelatorioAppService.cs |
| Low Coupling | GRASP | VendaFactory.cs (usa SaborSnapshot, não Sabor) |
| High Cohesion | GRASP | Lote.cs (CostPerUnit), Venda.cs (Total) |
| Information Expert | GRASP | Venda.cs (Total), RelatorioService.cs |
| Creator | GRASP | VendaFactory.cs, LoteFactory.cs |