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

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

Tags: | | |

Sobre o Autor

Adriano Ribeiro
Adriano Ribeiro

Profissional de tecnologia, agilista atuando no mercado a mais de 20 anos em diversas áreas. Nos últimos 5 anos venho trabalhando especificamente com pesquisa, desenvolvimento e inovação em diversos setores da indústria brasileira e internacional sempre utilizando conceitos e ou tecnologia de ponta. Há 19 anos eu era um novato em tecnologia e já havia criado meu primeiro portal para compartilhar informações de qualidade em língua portuguesa, o http://www.geleira.org. Acho que agora é o momento de voltar fazer isso e compartilhar o que aprendi nesses quase 20 anos de carreira.

0 Comentários

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Solicitar exportação de dados

Utilize este formulário para solicitar uma cópia dos seus dados neste site.

Solicitar remoção de dados

Utilize este formulário para solicitar a remoção dos seus dados neste site.