Back home

Ocultação e transferência de complexidade em "Otimização de manutenção"

Quando a complexidade é simplesmente transferida de grandes funções para a hierarquia de classes, configuração e cadeias de chamadas, o sistema geralmente não é mais sustentável.

Quando muitas equipes fazem “otimização da manutenção”, o primeiro passo é desmontar o código.

Uma função de 300 linhas foi dividida em 12 classes; um processo com muitas ramificações foi alterado para “estratégia + fábrica + configuração”; uma lógica que poderia ser entendida seguindo a chamada foi transformada em eventos, assinantes, tabelas de regras e vários diretórios de aparência limpa.

O código é realmente menos lotado e um único arquivo é mais curto. Ao revisar, dá até às pessoas a sensação de “isso é avançado”.

Mas meu julgamento é: **Muitas das chamadas otimizações de manutenção não reduzem a complexidade, mas apenas alteram a complexidade de parcialmente visível para dispersa, instável e oculta. ** O resultado mais comum desse tipo de mudança é que o problema é mais difícil de localizar, a mudança é mais difícil de avaliar e é mais difícil para novas pessoas entenderem.

O núcleo da manutenibilidade sempre foi: **Quando os requisitos mudam, ocorrem erros on-line e surgem condições de limite, a equipe consegue ver rapidamente as restrições reais e fazer modificações seguras dentro de um escopo limitado? **

A complexidade não desaparecerá por causa da divisão, apenas ficará em outro lugar.

Uma situação comum é que a intuição de “manutenção” seja muito visual.

Eles se sentem desconfortáveis ​​quando veem grandes funções, ficam atrasados ​​quando veem muitos if/else e instintivamente querem desmontá-los quando veem vários julgamentos de negócios enfiados em uma classe. Assim, a lógica complexa foi dividida em muitos arquivos finos, ramificações condicionais foram traduzidas em níveis de objeto, regras de negócios foram movidas para configurações e um pouco de interface e nomenclatura foram adicionadas, e a superfície do código ficou imediatamente mais limpa.

O problema é que embora as 300 linhas de código originais sejam feias, a complexidade está pelo menos espalhada na área de trabalho. Lendo de cima para baixo, você pode ver como as condições de ramificação, o estado compartilhado, o tratamento de exceções e os resultados finais estão conectados.

Uma vez quebrada a complexidade, a situação muda:

  • Você precisa percorrer 7 arquivos ao longo da cadeia de chamadas para saber onde um campo foi finalmente alterado;
  • Você precisa entender a interface, a classe de implementação, a lógica de registro e a montagem do runtime ao mesmo tempo para confirmar qual branch você está pegando;
  • Superficialmente, parece que as regras de negócios estão no código e os resultados estão metade em YAML, metade no banco de dados e metade em uma tabela de mapeamento gerada na inicialização.

A complexidade não diminuiu, mas passou de “um pouco cansativo na leitura” para “muito mais lento na localização de problemas”.

O custo de manutenção geralmente é liquidado três meses depois, quando alguém corrige a lógica errada, falha on-line e soluciona o problema do link.

O erro de julgamento mais comum das equipes é confundir “limpeza parcial” com “manutenção geral”

Esse tipo de erro de julgamento é comum porque muitos benefícios da refatoração parecem ser reais no curto prazo.

Por exemplo, uma função que contém diversas ramificações de processamento de pedidos pode ser alterada para a seguinte estrutura:

Handler h = handlerFactory.get(order.type());
h.validate(order);
h.price(order);
h.persist(order);
h.notify(order);

Este código certamente parece mais limpo do que uma longa lista de ramificações.

Mas a verdadeira questão é:

  1. Como decidir qual implementação usar para handlerFactory;
  2. Existe algum pré-requisito compartilhado entre validate/price/persist/notify;
  3. Se a deriva comportamental é permitida entre diferentes implementações;
  4. Uma determinada alteração de requisito deve ser feita em um local, em quatro locais ou em uma dúzia de locais?

Se esses problemas não forem restritos, então esse tipo de “estrutura elegante” muitas vezes apenas reescreve as diferenças de negócios que foram originalmente escritas explicitamente em if/else em diferenças implícitas espalhadas na hierarquia de classes.

Do ponto de vista da revisão, torna-se mais limpo; do ponto de vista da manutenção, torna-se mais dependente do contexto.

**Manutenção refere-se a se todo o sistema é mais fácil de responder “Onde essa mudança afetará?” **

O que realmente determina os custos de manutenção geralmente são quatro coisas

Prefiro usar as quatro perguntas a seguir para julgar se uma refatoração torna o sistema mais sustentável.

1. Quando o problema ocorre, o caminho do local é mais curto?

Ao relatar um problema online de que “certos tipos de pedidos ocasionalmente emitem cupons duplicados”, o mais valioso é se o engenheiro pode encontrar rapidamente: onde estão as condições de julgamento, onde está a proteção idempotente e onde os efeitos colaterais são acionados.

Se, após a divisão, o caminho da solução de problemas mudar de “observar uma função” para “examinar a definição da interface, encontrar a classe de implementação, verificar a montagem, rastrear eventos e inverter a configuração”, então o custo de manutenção realmente aumentará.

2. Quando os requisitos mudam, o escopo da modificação é mais convergente?

Uma boa abstração mantém o foco nas mudanças. A má abstração permite que a mudança se espalhe.

O pior tipo de refatoração é dividir a lógica em múltiplas responsabilidades superficialmente. Na verdade, toda vez que os requisitos mudam, eles devem ser alterados simultaneamente: definição de regras, registro de fábrica, configuração padrão, amostras de teste e pontos de monitoramento. O arquivo ficou menor, mas a área de alteração ficou maior.

Esse tipo de sistema parece modular, mas na verdade é mais frágil, porque toda vez que você faz uma mudança, tem que apostar que não perdeu nenhuma curva.

3. As restrições estão se tornando mais visíveis em vez de mais ocultas?

A razão pela qual muita lógica de negócios é difícil é que superficialmente parece que o código é feio, mas na verdade está mais próximo disso e tem muitos pré-requisitos:

  • Este estado só pode ir de A para B, não diretamente para C;
  • Este campo só pode ser modificado por determinados tipos de clientes;
  • Esta ação deve ter sucesso com outro efeito colateral.

Se após a refatoração essas restrições não aparecerem mais em um local, mas estiverem espalhadas em múltiplas classes, anotações, configurações ou ouvintes, existe o risco de amnésia.

4. O feedback do teste está mais próximo do comportamento real?

Muitas “otimizações de manutenção” levarão convenientemente a vários testes únicos que são fáceis de escrever porque cada classe é menor e as dependências são eliminadas.

No entanto, o aumento do número de testes únicos não significa que o sistema seja mais fácil de melhorar.

Se o teste puder apenas provar “esta classe retornará o valor esperado no mundo simulado”, mas não puder cobrir os relacionamentos de montagem, o estado compartilhado e as restrições de tempo no processo real, então se trata mais de proteger a estrutura do que de proteger o comportamento.

Um mal-entendido comum: para eliminar if/else, reescreva as diferenças de negócios em um sistema de tipos

É claro que if/else pode ser mal escrito, mas “eliminar if/else” não é um objetivo em si.

Já vi muitos sistemas que originalmente tinham apenas duas ou três ramificações claras e uma semântica de negócios muito estável. Entretanto, para buscar o padrão de projeto correto, eles foram divididos em interfaces políticas, classes base abstratas, centros de registro e pontos de extensão. Meio ano depois, o número de tipos aumentou de 3 para 9, mas tornou-se cada vez mais difícil para os chamadores julgar quais diferenças eram diferenças comerciais reais e quais eram apenas diferenças estruturais que sobraram da evolução histórica.

Em muitos casos, ter muitas ramificações não significa que o modelo de objetos deva ser adotado; significa apenas que há julgamento comercial aqui. A primeira coisa a fazer é distinguir quais destes julgamentos são eixos estáveis ​​de mudança e quais são apenas bifurcações condicionais no mesmo processo.

Se forem apenas alguns julgamentos condicionais em um processo, forçá-los a serem “orientados a objetos” provavelmente apenas reescreverá as condições que podem ser vistas rapidamente em várias camadas de envio de métodos.

**Ocultar a condição no polimorfismo não fará com que a condição desapareça, apenas fará com que o leitor perceba sua existência mais tarde. **

Outro mal-entendido comum: tratar a configuração como uma lixeira de complexidade

Outra abordagem que é particularmente fácil de ser confundida com “mais sustentável” é configurar regras de negócios tanto quanto possível.

Os motivos costumam ser muito bons: não há necessidade de alterar o código no futuro, o funcionamento é configurável e a expansão é mais flexível.

Mas a configuração não é inerentemente mais barata, apenas transfere a complexidade do tempo de compilação para o tempo de execução.

Quando uma configuração de regra começa a assumir muita responsabilidade, estes problemas podem surgir rapidamente:

  • Existe uma relação de prioridade e cobertura entre as configurações, mas não há nenhum local no sistema onde elas possam ser visualizadas na íntegra;
  • Quais cenários são afetados por uma alteração só podem ser verificados online;
  • Valores legais de configuração não significam semântica correta, erros serão expostos em tempo de execução;
  • A revisão do código torna-se “Não consigo entender o que esse JSON significa”.

Se uma regra muda frequentemente, mas a mudança ainda requer julgamento de engenharia, testes de ligação e planos de reversão, então é essencialmente um problema de código e não se tornará subitamente um item de manutenção de baixo custo apenas porque está escrito na configuração.

Um custo comum da configuração excessiva é que “ninguém mais se atreve a mexer no sistema”.

Contra-exemplo: Algumas abstrações realmente tornarão o sistema mais sustentável

Nem pode ser dito como “Não abstraia, não divida”.

Existem situações em que a abstração não só vale a pena, mas é necessária.

Por exemplo:

  • Enfrentar eixos de mudança estáveis e claros, como diferentes backends de armazenamento, diferentes canais de pagamento e diferentes protocolos de serialização;
  • Essas mudanças realmente precisam ser substituídas em tempo de execução, ao invés de apenas “possíveis extensões posteriores” imaginárias;
  • Cada implementação pode obedecer ao mesmo conjunto de restrições fortes, em vez de aparentemente ter a mesma interface, mas na verdade ter semânticas diferentes;
  • Os limites da equipe também seguem limites abstratos e diferentes módulos podem ser desenvolvidos e testados de forma independente.

O valor da abstração neste ponto é que ela realmente reduz o atrito de mudanças futuras.

Da mesma forma, se uma função longa for responsável pela verificação de parâmetros, tomada de decisões de negócios, orquestração de efeitos colaterais e compensação de exceções, geralmente é correto dividi-la em várias etapas com limites claros. A premissa é que após a desmontagem, a espinha dorsal do processo ainda esteja visível e as principais restrições não fiquem ocultas.

Portanto, a questão sempre foi: depois de concluída a demolição, a complexidade é contida ou é apenas transferida para outros cantos cognitivos. **

Um método de julgamento mais prático: primeiro observe as modificações mais comuns no futuro, não olhe primeiro para a limpeza estrutural de hoje

Se eu suspeitar que um “refatorador de manutenção” é apenas um polimento estrutural, geralmente começo fazendo três perguntas muito práticas:

  1. Da próxima vez que o produto alterar esse requisito, quais serão as mudanças mais prováveis para os engenheiros?
  2. Se algo der errado online na próxima vez, qual link o atendente deve consultar primeiro?
  3. Se uma nova pessoa assumir o comando, ela precisará primeiro entender as regras de negócios ou a estrutura da estrutura.

Se as respostas a estas três perguntas se tornarem mais complicadas, então há uma grande probabilidade de que esta refatoração não melhore a capacidade de manutenção.

A capacidade de manutenção é para custos de modificação futuros, não para capturas de tela de código de hoje.

Resumo

A razão pela qual muitas “otimizações de manutenção” são perigosas é que superficialmente elas parecem completamente inúteis, mas na verdade está muito mais próximo delas, sendo muito fácil parecer parcialmente correta.

Há mais classes, as funções ficaram mais curtas, o diretório ficou mais organizado e o processo de revisão ficou mais tranquilo. Mas o verdadeiro custo de manutenção vem da compreensão, posicionamento, modificação e verificação, e não da limpeza visual.

Portanto, minha sugestão é muito simples: **Não desmonte a lógica complexa até que ela fique invisível, primeiro desmonte a lógica complexa até que ela possa ser alterada. **

Se uma refatoração simplesmente transfere a complexidade do arquivo atual para a cadeia de chamadas, camada de configuração e camada de abstração, geralmente não melhora a capacidade de manutenção, mas apenas atrasa a dor da próxima solução de problemas.