Classes autônomas

No meio desse mar de conexões entre sistemas, classes, métodos, etc., a adoção de classes autônomas vem como um porto seguro, tentando trazer uma programação menos emaranhada, compreensível e flexível. A compreensão de que as dependências estão em toda a parte, evidencia que ações precisam ser feitas para que ainda sim haja coesão e coerência. Ao analisarmos as classes autônomas, destacamos a importância da alta coesão e do baixo acoplamento como pilares essenciais para o design de software. Os módulos e agregados vem como isoladores da complexidade gerada. Além disso os Value Objects e tipos primitivos trazem benefícios semelhantes. Por fim, lidar com técnicas como essa tenta controlar a carga cognitiva necessária para a compreensão do software, aumentando a capacidade do desenvolvedor em entender o cenário amplo de software desenvolvido.

A busca incessante por estruturas de código mais compreensíveis e ‘evoluíveis‘ levou à concepção do que Eric Evans chamou de Classes Autônomas. Em meio à complexidade inerente à programação, é importante entender que quase tudo que se faz gera dependências. Seja na relação entre classes, módulos ou sistemas distintos, a interdependência é onipresente. Contudo, é na análise refinada dessas dependências que se observa o valor das classes autônomas, sendo importante na construção de softwares mais simples de serem entendidos e trabalhados.

Aqui no blog temos um grande conjunto de artigos sobre Domain Driven Design. Nenhum deles é requisito para a leitura desse, mas pode ser complementar:

Tudo é dependência

Precisamos ter clareza de que na codificação quase tudo é dependência. É fácil notar isso na relação entre classes, entre módulos ou até entre diferentes sistemas. Mas observe que mesmo em um método com apenas um parâmetro há dependências. Se uma classe tem uma relação com outra temos 3 coisas para nos preocupar: a classe 1, a classe 2 e a relação entre elas. Quando mais relações mais complexo é o cenário e pior é para a evolução do domínio.

Entretanto veja que nem toda dependência é ruim ou cria complexidade acidencial, algumas são mínimas e fundamentais para representar os conceitos e intenções do domínio. Então devemos procurar essas boas dependências e tratar as más através de refatorações constantes.

Alta coesão e baixo acoplamento de novo

Em diversos artigos diferentes aqui do blog acabo voltando para o tema do alta coesão e o baixo acoplamento. As estruturas programadas precisam ser coesas, ou seja, precisam cumprir o seu propósito específico, sem se preocupar necessariamente com a sua relação entre ela e outras estruturas. Veja que quando falo em estruturas você pode entender um método, uma clásse, um agregado ou mesmo um módulo, seguindo a visão estruturada do Domain Driven Design.

Por outro lado há o acoplamento. Estou falando, então, da relação entre uma estrutura e outras, sugerindo que haja coerência entre as relações. Mas além disso é fundamental que a quantidade de relações seja o mínima possível, nem a mais nem a menos. Refatorações constantes devem perseguir esse objetivo até que se encontre o ponto ótimo.

Módulos e Agregados controlam as dependências

Módulos e agregados surgem no DDD como ferramentas para o controle do emaranhado de dependências que vão surgindo no desenvolvimento. Um agregado é feito para ser autocontido como uma unidade transacional finita. Ele não existe apenas por esse capricho, mas por que ele tem na sua alma a coesão; Além da garantia de que só lida com aquela complexidade definida pelas classes que o envolvem. Pessoalmente acho primoroso o trabalho que o Evans fez.

A mesma coisa ocorre para os módulos, mas numa escala maior. Um módulo tem por objetivo garantir certo nível de coerência entre os agregados que possui, limitando também a quantidade de conexões possíveis. Um módulo é altamente coeso e de baixo acoplamento quando, em última análise, representa completamente um sub-domínio, criando a relação ideal entre a linguagem ubíqua e a codificação.

Mas é claro que módulos e agregados não eliminam o esforço de se entender o sistema por dentro, mas quando bem estruturado fica fácil de acolher novos desenvolvedores, e as intenções e conceitos ficam mais claros. Veja que há uma luta constante entre a complexidade acidental e natural e é dever do desenvolvedor perceber e otimizar. Caso contrário a evolução do sistema trará custos desnecessários.

Classes autônomas, conceitos implícitos e carga cognitiva

Outro ponto que merece um destaque são os conceitos implíticos. Quando uma estrutura não está ótima, seja um módulo que não representa completamente o domínio, seja um agregado que é constatemente refatorado, sabemos que há conceitos não extraídos. Muitas vezes esses conceitos são representações complexas de agregados inteiros, relações entre egregados, mas em outras nem tanto. A questão é que o fato de não ter esses conceitos explícitos significará maior esforço no entendimento da estrutura em questão, logo maior necessidade de cognição do desenvolvedor. Não veja isso apenas como uma questão técnica já que isso significará mais custos para os sistemas, maior necessidade de refatoração e algum nível de desalinhamento entre os desenvolvedores e os usuários especialistas.

Classes autonomas, Value Objects e tipos primitivos

Evans em seu livro sugere, então, para um design mais flexível no desenvolvimento as Classes autônomas. Essas são classes que visam representar idealmente um Conceito do domínio. Muitas vezes os Value Objects servem muito bem para esse propósito, por serem bem concisas e imutáveis. Note que compreender um Value Object é, de maneira geral, muito simples. Essa simplificade é a chave para a redução das dependências que ele tanto critica.

Outro ponto curioso é que tipos primitivos oferecem uma vantagem frente à classes sofisticadas. Um tipo primitivo é um conceito cristalino para todo desenvolvedor e, portanto, oferece um nível de entendimento mais imediado. Para Evans, deve-se considerar o uso de primitivos para reduzir não apenas a complexidade computacional mas também a de entendimento. Mas, por favor, não entenda errado utilizando primitivos ou supersimplificando um código desnecessariamente.

Quero ver código: Hipótese de uso

Veja um exemplo simples mas elucidativo para o cenário das classes autonomas. Inicialmente vamos ver um método simples que envia dados, sem uma preocupação específica com o negócio. Mas note que um endereço deve ser dado como parâmetro, com o tipo string. De fato não é algo absolutamente claro.


public class ExemploEnvio
{
    // Método Connect que recebe um endereço IP
    private void Connect(string endereco)
    {
        // Lógica de conexão aqui
        Console.WriteLine($"Conectando ao endereço: {endereco}");
    }

    // Método Enviar que chama o método Connect
    public void Enviar(string conteudo, string endereco)
    {
        // Lógica de envio aqui

        // Chamando o método Connect
        Connect(endereco);

        // Mais lógica de envio, se necessário
        Console.WriteLine($"Enviando conteúdo: {conteudo}");
    }
}

Vamos refatorar esse exemplo criando um Value Object que representa esse conceito de endereço. Vamos esclarecer que se trata apenas de um endereço IP. Nesse cenário será mais simples entender o uso completo da aplicação para novatos.

// Value Object para representar um endereço IP
public class EnderecoIP
{
    public string Valor { get; }

    // Construtor que valida o formato do endereço IP
    public EnderecoIP(string valor)
    {
        // Lógica de validação do endereço IP (pode ser mais elaborada conforme necessário)
        // Aqui, estamos assumindo uma validação simples para fins de exemplo
        if (!IsEnderecoIPValido(valor))
        {
            throw new ArgumentException("Formato de endereço IP inválido", nameof(valor));
        }

        Valor = valor;
    }

    private bool IsEnderecoIPValido(string valor)
    {
        // Lógica de validação do endereço IP
        // Pode ser mais robusta dependendo dos requisitos do seu sistema
        // Por exemplo, verificar se o formato está correto, se os octetos estão no intervalo válido, etc.
        return true; // Implemente a validação adequada aqui
    }
}

public class ExemploEnvio
{
    // Método Connect que recebe um Value Object EnderecoIP
    private void Connect(EnderecoIP endereco)
    {
        // Lógica de conexão aqui
        Console.WriteLine($"Conectando ao endereço: {endereco.Valor}");
    }

    // Método Enviar que chama o método Connect
    public void Enviar(string conteudo, EnderecoIP endereco)
    {
        // Lógica de envio aqui

        // Chamando o método Connect
        Connect(endereco);

        // Mais lógica de envio, se necessário
        Console.WriteLine($"Enviando conteúdo: {conteudo}");
    }
}

Entretanto o exemplo anterior recebe os dados como string, algo que facilita o uso e é uma boa alternativa. O exemplo a seguir exagera a questão e realiza uma validação dos dados através de seu tipo primitivo, sem necessidade de nenhuma lógica específica e com um design completamente coeso. Veja que isso não é uma obrigatoriedade, mas é algo que remove qualquer tipo de dúvida sobre o conceito tratado pelo problema em questão.

// Value Object para representar um endereço IP
public class EnderecoIP
{
    public string Valor { get; }

    // Construtor que recebe os octetos do endereço IP
    public EnderecoIP(byte octeto1, byte octeto2, byte octeto3, byte octeto4)
    {
        Valor = $"{octeto1}.{octeto2}.{octeto3}.{octeto4}";
    }
}

public class ExemploEnvio
{
    // Método Connect que recebe um Value Object EnderecoIP
    private void Connect(EnderecoIP endereco)
    {
        // Lógica de conexão aqui
        Console.WriteLine($"Conectando ao endereço: {endereco.Valor}");
    }

    // Método Enviar que chama o método Connect
    public void Enviar(string conteudo, EnderecoIP endereco)
    {
        // Lógica de envio aqui

        // Chamando o método Connect
        Connect(endereco);

        // Mais lógica de envio, se necessário
        Console.WriteLine($"Enviando conteúdo: {conteudo}");
    }
}

class Program
{
    static void Main()
    {
        ExemploEnvio exemplo = new ExemploEnvio();

        // Chamar o método Enviar com um conteúdo e um endereço IP representado por quatro bytes
        exemplo.Enviar("Conteúdo para envio", new EnderecoIP(192, 168, 0, 1));
    }
}

Conclusão de Classes autônomas

No meio desse mar de conexões entre sistemas, classes, métodos, etc., a adoção de classes autônomas vem como um porto seguro, tentando trazer uma programação menos emaranhada, compreensível e flexível. A compreensão de que as dependências estão em toda a parte, evidencia que ações precisam ser feitas para que ainda sim haja coesão e coerência.

Ao analisarmos as classes autônomas, destacamos a importância da alta coesão e do baixo acoplamento como pilares essenciais para o design de software. Os módulos e agregados vem como isoladores da complexidade gerada. Além disso os Value Objects e tipos primitivos trazem benefícios semelhantes. Por fim, lidar com técnicas como essa tenta controlar a carga cognitiva necessária para a compreensão do software, aumentando a capacidade do desenvolvedor em entender o cenário amplo de software desenvolvido.


Thiago Anselme
Thiago Anselme - Gerente de TI - Arquiteto de Soluções

Ele atua/atuou como Dev Full Stack C# .NET / Angular / Kubernetes e afins. Ele possui certificações Microsoft MCTS (6x), MCPD em Web, ITIL v3 e CKAD (Kubernetes) . Thiago é apaixonado por tecnologia, entusiasta de TI desde a infância bem como amante de aprendizado contínuo.