Quando Obsessão Por Tipos Primitivos se Tornam um Problema
A final obsessão por tipos primitivos é um problema ou uma bençam? Como todo bom desenvolvedor sênior, vou me dar ao luxo que usar a carta do "depende...". Mas afinal, que motivos poderiam dar motivos para ambas as sensações?
Alguns pontos importante precisam ser considerados, pois um padrão e práticas não são bons por que se aplicam a todos os cenários, mas sim por funcionarem bem quando aplicados em um cenário com as pré-condições as quais eles se propõem a resolver existem. É justamente por esse motivo que técnicas de agilidade são relativamente simples de serem entendidas, mas as combinações são diferentes em cada empresa.
Técnicas de desenvolvimento não se aplicam a todos os cenários
Trabalhei com pesquisa e desenvolvimento durante 8 anos, as entregas "finais" dos projetos eram em geral feitas entre 3 e 12 meses. Durante esse período haviam entregas parciais já com algum resultado prático para o parceiro. Será que eu utilizava as mesmas técnicas em todos os projetos? Claro que não! Times, tecnologias, perfis, expertise e tipo de entrega mudavam em cada projeto e devido a isso os conjuntos para entrega de valor precisavam ser diferentes.
Aplicando em todos os lugares
Quando eu descobri a Técnica de Obsessão por tipos primitivos passei a aplicação indistintamente em todos os objetos da aplicação, excetuando-se DTOs para persistência e comandos de entrada. Eu fiz isso porque eu estava aplicando o padrão de projeto Objeto de Valor aka Value Object a todos os objetos que representavam conceitos de negócio na aplicação.
O uso na criação de comandos que são passados como parâmetros para camada de serviço de domínio.
public class AddProjectCommand : BaseCommand
{
public AddProjectCommand(string name, string owner, string code, DateTime startDate, decimal budget,
Guid clientId, string serviceOrderNumber, bool serviceOrderStatus, string status)
{
Name = ProjectName.From(name);
ServiceOrderNumber = ServiceOrder.From((serviceOrderNumber, serviceOrderStatus));
Status = ProjectStatus.From(status);
Code = ProjectCode.From(code);
StartDate = DateAndTime.From(startDate);
Budget = Money.From(budget);
ClientId = EntityId.From(clientId);
Owner = Email.From(owner);
AppendValidationResult(Name.ValidationStatus.ToFailures());
AppendValidationResult(ServiceOrderNumber.ValidationStatus.Failures);
AppendValidationResult(Status.ValidationStatus.ToFailures());
AppendValidationResult(Code.ValidationStatus.ToFailures());
AppendValidationResult(StartDate.ValidationStatus.ToFailures());
AppendValidationResult(Budget.ValidationStatus.ToFailures());cs
AppendValidationResult(ClientId.ValidationStatus.ToFailures());
AppendValidationResult(Owner.ValidationStatus.ToFailures());
}
public ProjectName Name { get; set; }
public Email Owner { get; set; }
public ProjectCode Code { get; set; }
public DateAndTime StartDate { get; set; }
uso em eventos de domínio ProjectAddedEvent abaixo e em uso na agregação
public class ProjectAddedEvent : DomainEvent
{
private ProjectAddedEvent(EntityId id, ProjectName name, ProjectCode code, DateAndTime startDate, Money budget,
EntityId clientId, VersionId version)
: base(DateTime.Now, version)
{
Id = id;
Code = code;
Name = name;
Budget = budget;
StartDate = startDate;
ClientId = clientId;
Owner = Email.Empty();
Status = ProjectStatus.Default();
OrderNumber = ServiceOrder.Empty();
}
public EntityId Id { get; }
public ProjectName Name { get; }
public ProjectCode Code { get; }
public Money Budget { get; }
A questão é que eventos são gerados dentro do objeto que garante a execução das transações de negócio inforçando todas as regras de negócio.
public sealed class ProjectAggregationRoot : ObjectBasedAggregationRootWithEvents<Project, EntityId>
{
public ProjectAggregationRoot(Project project)
{
Debug.Assert(project.IsValid);
Apply(project);
if (project.IsNew())
{
Raise(ProjectAddedEvent.For(project));
}
}
public void UpdateDetail(Project.ProjectDetail detail, ISpecification<Project> specUpdateProject)
{
var projUpdated = Project.CombineWith(AggregateRootEntity, detail);
if (specUpdateProject.IsSatisfiedBy(projUpdated) == false)
{
AppendValidationResult(projUpdated.Failures);
}
else
{
Apply(projUpdated);
Raise(ProjectDetailUpdatedEvent.For(projUpdated));
}
}
Vantagens do padrão Objeto de Valor para ilustrar algumas
- Facilidade para escrita de testes de unidade e micro testes
- Redução da duplicação de código e ocultação
- Encapsulamento das regras de negócio do objeto SOLID
- Melhora a expressividade DDD e tempo gasto lendo código
O mundo foi maravilhoso enquanto a camada de domínio de negócio era escrita, mas quando o padrão objeto de valor era usado, mas ao integrar com demais tendo os objetos de domínio como interface nas camada de serviços e persistência, isso se tornava um terror, pois essas camadas em geral exportamos ou recebemos dados, já que elas atuam como adapter ou gateway conforme Arquitetura Limpa, ou seja interagimos com o "mundo exterior" e toda a segurança implementa se torna um problema quando não usamos objetos puros, como os objeto POCO ou POJO como preferir.
O problema da "Super-proteção" Over-engineering
Foi mais uma lição sobre over-engineering que tanto se fala. Pois eu fazia todas as validações para criar o objeto de negócio e queria que as camadas de serviço de domínio e persistẽncia "herdassem" as regras de negócio encapsuladas no objetos de valor, mas isso é totalmente desnecessário uma vez que os objetos de persistência são criados a partir da entidade de negócio que inforça as regras de negócio implícitas aos objectos de valor que a compõem bem como as suas próprias relativas aos seus relacionamentos com outras entidades, limiares e faixas de valores definidos pelas regras de negócio internas e externas a entidade.
protected override async Task<CommandResult<Guid>> ExecuteCommand(
AddProjectCommand command,
CancellationToken cancellationToken)
{
var isSucceed = false;
var aggregationId = Guid.Empty;
```csharp
var client = _dbUserSession.Repository.Get(command.ClientId);
var agg = _factory.Create(command);
agg.AddProject(client, new ProjectCanBeAddedToClient());
if (agg.IsValid)
{
await _dbSession.Repository.Add(agg.GetChange());
await _dbSession.SaveChangesAsync(cancellationToken);
agg.GetEvents().ToImmutableList()
.ForEach(ev =>
Publisher.Publish(ev, cancellationToken));
isSucceed = true;
aggregationId = agg.GetChange().Identity.Value;
}
return new CommandResult<Guid>(isSucceed, aggregationId, agg.Failures);
```
var client = _dbUserSession.Repository.Get(command.ClientId);
var agg = _factory.Create(command);
agg.AddProject(client, new ProjectCanBeAddedToClient());
if (agg.IsValid)
{
await _dbSession.Repository.Add(agg.GetChange());
await _dbSession.SaveChangesAsync(cancellationToken);
agg.GetEvents().ToImmutableList()
.ForEach(ev =>
Publisher.Publish(ev, cancellationToken));
isSucceed = true;
aggregationId = agg.GetChange().Identity.Value;
}
return new CommandResult<Guid>(isSucceed, aggregationId, agg.Failures);
Usando Objeto de Valor na camada de persistência a propriedade VersionId do registro persistido é convertida para um objeto para ser verificada.
var version = VersionId.From(BitConverter.ToInt32(oldState.RowVersion));
if (VersionId.Next(version) > entity.Version)
throw new DbUpdateConcurrencyException("This version is not the most updated for this object.");
DbContext.Entry(oldState).CurrentValues.SetValues(entry);
Onde aplicar então?
A camada de domínio é o melhor lugar para se explorar o uso dessa técnica, pois é necessário orquestrar, encapsular e organizar os vários tipos de regras de negócio como forma de tornar mais explícitas as etapas do processo implementada reduzindo assim o tempo necessário para entendimento do código por parte do desenvolvedor.
Abaixo segue um exemplo de método utilizando a camada da camada de serviço
public async Task
Execute(ProductCreate command, CancellationToken cancellationToken) { var aggregate = ProductAggregationRoot.Create(ProductName.From(command.Name), ProductDescription.From(command.Description), ProductWeight.From(command.Weight)); if (aggregate.IsValid) { await this._sessionDb.Repository.Add(aggregate.GetChange()); await this._sessionDb.SaveChangesAsync(cancellationToken); return aggregate.GetChange().ToResultSucced(); } return aggregate.GetChange().ToResultFailed(); }
Segue um evento como um objeto record puro
public class ProductCreatedEvent : DomainEvent
{
public ProductCreatedEvent(ProductId id, ProductName name, ProductDescription description
, ProductWeight weight)
: base(DateTime.Now)
{
Id = id.Value;
Name = name.Value;
Description = description.Value;
Weight = weight.Value;
}
public Guid Id { get; }
public string Name { get; }
public string Description { get; }
public double Weight { get; }
Aplicar a todos os objetos da aplicação é code-smell pois é uma redundância e ainda torna o processo de serialização mais complexo devido a "contaminação" com propriedades de encapsulamento específicas do Objeto de Valor.
Imagem da capa: https://www.flickr.com/photos/danandkir/316158013
Sobre o Autor
0 Comentários