Desenvolver software é mais do que um exercício técnico: é uma capacidade de traduzir o negócio em algo tangível e repetitível. Nesse sentido o Domain-Driven Design (DDD) vem como uma forma de não apenas refletir o domínio de um sistema, mas de comunicar claramente as intenções subjacentes ao código. Este artigo mergulha nas nuances das Interfaces Reveladoras de Intenções, explorando como o design de software pode se tornar um diálogo claro entre os desenvolvedores e o domínio, garantindo não apenas a funcionalidade, mas que ele seja continuamente compreensível e manutenível.
Então, aqui no blog temos vários artigos sobre vários temas, entre eles Domain Driven Design. Selecionei alguns artigos que não são obrigatórios para a leitura desse, mas podem ser um bom complemento.
- Domain Driven Design tático
- Domain Driven Design estratégico
- Criando Context maps DDD com o VS Code
- Desvendando o Context Map
Sumário
Software para o negócio e Software para o desenvolvedor
A maior parte dos textos que encontro na internet sobre DDD consideram o domínio como o foco, claro, mas ignoram que o programador não é apenas um produtor mas também consumidor de código. Assim, um código bem escrito suporta designs sofisticados e acolhem novos programadores com maior facilidade. Então, o contexto desse artigo explora o fato de que para se construir um design que continuamente represente o domínio o programador-usuário é elemento fundamental. Construir códigos que sejam de fácil compreensão para além do clichê, considerando que:
- os encapsulamentos devem evitar que o desenvolvedor tenha que olhar por dentro para entender;
- as abstrações devem ser claras sobre sua intenção;
- os nomes devem estar ligados ao domínio ubíquo;
- a intenção é o fundamento do design;
Intenções Claras, Código Forte
As intenções de um dado software são a fundação de seu design e devem ser observadas por todos e preservadas a todo o custo. Veja, não estou falando que o software deve entender que segurança é mais ou menos importante do que desempenho. Ou que outros requisitos devem ter menos destaque. Quero dizer que tudo o que for feito em um software deve refletir as suas intenções, caso contrário o design vai ruir.
Quando um desenvolvedor não observa a intenção envolvida ele escreve métodos, variáveis, delegates, predicados, etc. com base em seus gostos particulares. Isso gera inconsistência entre seus contextos e pedirá refatorações ou migrações no futuro. O melhor é deixar isso bem alinhado já na largada, com programadores prontos para esse entendimento.
Interfaces reveladoras de intenções, em exemplos: Reserva de Salas de Reunião
Veja que por intenção estou falando em fazer bem o “feijão com arroz”, nomeando bem as variáveis, nome de classes, etc. mas sempre refletindo o que se pretende. Mas, nem sempre isso fica completamente claro por existirem tipos primitivos que poderiam estar empacotados em classes que dariam mais significado. Ou também poderia haver testes de unidade que dariam ainda mais a clareza da intenção. Vamos a um exemplo simples, de um código que não revela bem suas intenções e depois sua correção.
public class ReservaSalaService
{
public void ReservarSala(int s, DateTime i, DateTime f, Responsavel r, Prioridade p)
{
// Implementação para reservar sala
// O desenvolvedor precisa ler o código para entender como funciona a prioridade
}
}
O código a seguir remove o parâmetro prioridade e o transforma em métodos distintos que expressam melhor seu significado. Além disso, há um método que pode receber a prioridade como booleano, mas ela direciona para o método correto. E também há um tipo definido para o retorno chamado Reserva que ajuda a esclarecer o ciclo de vida desse objeto.
public class AgendadorSalaReuniao
{
public Reserva ReservarSala(Sala sala, DateTime inicio, DateTime fim, Responsavel responsavel, bool possuiPrioridade)
{
if (possuiPrioridade)
return ReservarSalaPrioritaria(sala, inicio, fim, responsavel);
else
return ReservarSalaComum(sala, inicio, fim, responsavel);
}
private Reserva ReservarSalaPrioritaria(Sala sala, DateTime inicio, DateTime fim, Responsavel responsavel)
{
// Implementação para reserva de sala prioritária
// Aqui você criaria a instância de Reserva específica para uma reserva prioritária
return new Reserva(sala, inicio, fim, responsavel, true, StatusDaReserva.Confirmada);
}
private Reserva ReservarSalaComum(Sala sala, DateTime inicio, DateTime fim, Responsavel responsavel)
{
// Implementação para reserva de sala comum
// Aqui você criaria a instância de Reserva específica para uma reserva comum
return new Reserva(sala, inicio, fim, responsavel, false, StatusDaReserva.Pendente);
}
}
Observando mais cuidadosamente a classe Reserva é fácil de notar a enumeração Status. Ela é fundamental para entender a consequencia do uso dos métodos de reserva, algo importante para que o código seja testável na sua unidade.
public class Reserva
{
public Sala Sala { get; }
public DateTime Inicio { get; }
public DateTime Fim { get; }
public Responsavel Responsavel { get; }
public bool PossuiPrioridade { get; }
public StatusDaReserva Status { get; }
public Reserva(Sala sala, DateTime inicio, DateTime fim, Responsavel responsavel, bool possuiPrioridade, StatusDaReserva status)
{
Sala = sala;
Inicio = inicio;
Fim = fim;
Responsavel = responsavel;
PossuiPrioridade = possuiPrioridade;
Status = status;
}
}
public enum StatusDaReserva
{
Pendente,
Confirmada,
Cancelada
}
Para finalizar veja o teste de unidade. Ele é mais do que parece: ele é outra confirmação da intenção envolvida nesse desenvolvimento, e não apenas uma validação técnica do método.
[TestClass]
public class AgendadorSalaReuniaoTests
{
[TestMethod]
public void ReservarSala_ComPrioridade_ReservaPrioritariaRealizadaComStatusConfirmado()
{
var agendador = new AgendadorSalaReuniao();
var sala = new Sala("Sala de Reunião A");
var reservaPrioritaria = agendador.ReservarSala(sala, DateTime.Now, DateTime.Now.AddHours(1), new Responsavel(), true);
Assert.AreEqual(StatusDaReserva.Confirmada, reservaPrioritaria.Status, "A reserva prioritária não foi confirmada corretamente.");
}
[TestMethod]
public void ReservarSala_SemPrioridade_ReservaComumRealizadaComStatusPendente()
{
var agendador = new AgendadorSalaReuniao();
var sala = new Sala("Sala de Reunião B");
var reservaComum = agendador.ReservarSala(sala, DateTime.Now, DateTime.Now.AddHours(1), new Responsavel(), false);
Assert.AreEqual(StatusDaReserva.Pendente, reservaComum.Status, "A reserva comum não está no status pendente.");
}
}
Interface reveladora de intenções
Eric Evans (autor do livro Domain Driven Design: Atacando as complexidades no coração do software) cita em seu livro o termo cunhado por Kent Beck: Seletor relevador de intenções. O seletor é uma visão micro, indicando a intenção observável de uma variável, método, classe, etc.
Com base nesse, o Eric cunhou o termo Interface reveladora de intenções (intention-revealing interfaces), que é mais abrangente indicando um conjunto de pontos de contato entre o desenvolvedor e o código como uma interface. Portanto não confunda esse tempo com a palavra-reservada interface comum a varias linguagens.
Note que essa interface é uma conexão entre a linguagem ubíqua, o programador e o código. A intenção é do domínio e precisa ser bem traduzida através de interfaces pelo programador.
Abstrações e Clareza da Intenção
Na grande maioria das situações os desenvolvedores dão manutenção ou evoluem um software existente ao invés de criar do zero. Ambos os cenários têm suas complexidades. Muitas vezes temos códigos que passam por camadas, ou que usam polimorfismos sofisticados. Nesses casos a intenção pode ser ainda mais difícil de ser percebida.
Em cenários assim é importante termos em mente que um bom encapsulamento é aquele que não exige grandes esforços do desenvolvedor para entende-lo. As interfaces devem revelar suas intenções com baixo custo cognitivo. O exemplo da sala de reunião (no capítulo Intenções Claras, Código Forte) não demonstra esse tipo de complexidade, entretanto, as soluções empregadas no exemplo podem servir para cenários como esse.
Há uma questão interessante nessa temática, o paradoxo da complexidade. O software existe para que ações repetitivas possam ser escritas de modo a não ter que programar todas as repetições. Essas repetições podem ocorrer nos dados, nos tipos, nos métodos ou em agrupamentos maiores. Por conta disso a orientação a objetos ganhou tanta relevância. Mas, acontece que, o uso dessas práticas, que obviamente são construídas para reduzir a complexidade, geram complexidade. Elas que deveriam tornar o código mais claro podem gerar o efeito contrário. Estou falando isso mas não deixe de levar em consideração a intenção: se as abstrações destroem o propósito, repense seu código.
Conceitos implícitos e explícitos
Quando desenvolvemos um software de um dado domínio nem sempre os conceitos mais importantes são obtidos logo no inicio do processo. Isso acontece porque o usuário entrevistado tem esse conceito como algo tão cotidiano que não deu o destaque necessário no momento certo; ou pelo contrário, a TI pode estar tão ensimesmada que não vê alguns detalhes que o usuário oferece. Note que nesses casos o conceito em si pode ou não compor o software, mas em algumas situações ele fica implícito.
Voltando para a questão da intenção, note que conceitos implícitos são o contrário de intencionalidade. Isso significa que eles precisam ser observados e emergidos, através de refatorações constantes. Volte ao exemplo da sala de reuniões: inicialmente ela não tinha um objeto Sala, portanto não tinha o Status, algo realmente importante para o correto entendimento do sistema, mas a princípio passou batido.
Interfaces reveladoras de intenções e a Sobrecarga cognitiva
No mesmo sentido de termos as intenções bem esclarecidas com interfaces reveladoras de intenções, abstrações claras e conceitos explicitados, vamos falar de sobrecarga cognitiva. Esse tema é bem profundo e em algum momento farei um artigo apenas sobre ele.
Vamos contextualizar a sobrecarga sobre alguns diferentes pontos de vista. O primeiro é da arquitetura empresarial como um todo. Veja o diagrama a seguir que exemplifica bem. A empresa possui negócios e modelos de negócios que podem ser mais ou menos complexos. Depois há aplicações e conceitos chave (CRM, ERP, etc.) que se conectam de diversos modos com diversos negócios. Seguindo temos as tecnologias que fazem tudo isso se comunicar com conectores, bancos de dados, sistemas de fila, entre outros. Tudo isso orientado a grandes requisitos não funcionais como desempenho, segurança. Não é pouca coisa, e isso é apenas uma das visões.
Numa outra perspectiva, além do que comentamos há o conhecimento das pessoas envolvidas, do método de desenvolvimento, dos épicos, das estórias. E também dos blocos de código, do system-design (DDD, Clean Arch, etc.), da arquitetura tecnologica (3 camadas, EDA, microservice) e assim por diante. É realmente bastante coisa.
Note também que o uso correto das abstrações e encapsulamentos se relaciona diretamente com esse problema. Ao usar interfaces reveladoras de intenções há uma redução da carga cognitiva necessária para o desenvolvimento e manutenção dos sistemas. Portanto, reduzir essa carga propicia softwares mais robustos com complexidade mais natural e evitando a inserção de complexidade acidental.
Outra vantagem relevante para esse cenário é que ao se preocupar com a sobrecarga cognitiva reduz-se o risco de concentração de conhecimento em poucas pessoas, uma vez que o software é feito para o cliente, mas o código é feito para o outro dev.
Testes de Unidade, Comportamento e Intenção (TDD e BDD)
A simplicidade no design não é apenas uma questão estética: é uma estratégia essencial para reduzir a sobrecarga cognitiva. Quando o desenvolvedor escreve um código de teste ele faz mais do que apenas garantir que a funcionalidade segue o combinado. Ele garante que usou a funcionalidade como se fosse outro desenvolvedor. Esse detalhe não pode passar desapercebido! Ao testar há uma verificação de que os principais conceitos deixaram de ser implícitos e de que existe interface que pode ou não revelar as intenções. E além de tudo isso, o teste é uma documentação sobre como consumir a funcionalidade em questão.
Conclusão de Interfaces reveladoras de intenções
No artigo exploramos a essência de construir software significativo: a clareza nas intenções. A fundamentação de Interfaces Reveladoras de Intenções revela-se não apenas como uma prática técnica, mas como um compromisso fundamental com a comunicação eficaz entre o domínio e o código.
A clareza nas intenções, desde os nomes das classes até a construção de interfaces, não é uma mera formalidade, mas uma estratatégia consistente para redução da sobrecarga cognitiva, garantindo a longedidade do sistema. Ao abraçar abstrações claras, explicitar conceitos implícitos e adotar práticas como TDD e BDD, fortalecemos não apenas a robustez do código, mas também a capacidade de evolução dele.
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.