Quatro regras aprimoradas para design de software

Olá Habr! Apresento a vocês o artigo "Quatro Melhores Regras para Design de Software", de David Bryant Copeland. David Bryant Copeland é arquiteto de software e CTO da Stitch Fix. Ele mantém um blog e é autor de vários livros .


Martin Fowler recentemente twittou com um link para seu blog sobre quatro regras simples de design de Kent Beck, que eu acho que podem ser melhoradas (e às vezes podem levar o programador ao caminho errado):


As regras de Kent da programação extrema explicadas :


  • Kent diz: "Execute todos os testes".
  • Não duplique a lógica. Tente evitar duplicatas ocultas, como hierarquias de classes paralelas.
  • Todas as intenções importantes para o programador devem ser claramente visíveis.
  • O código deve ter o menor número possível de classes e métodos.

De acordo com minha experiência, essas regras não atendem perfeitamente às necessidades de design de software. Minhas quatro regras para um sistema bem projetado podem ser:


  • está bem coberto por testes e passa com êxito.
  • ele não possui abstrações que o programa não precisa diretamente.
  • ela tem um comportamento inequívoco.
  • requer a menor quantidade de conceitos.

Para mim, essas regras decorrem do que fazemos com nosso software.


Então, o que fazemos com o nosso software?


Não podemos falar sobre design de software sem antes falar sobre o que pretendemos fazer com ele.


O software foi escrito para resolver o problema. O programa é executado e tem um comportamento. Esse comportamento é estudado para garantir a operação correta ou detectar erros. O software também muda frequentemente para fornecer um comportamento novo ou alterado.


Portanto, qualquer abordagem ao design de software deve se concentrar em prever, estudar e entender seu comportamento, a fim de tornar a alteração desse comportamento o mais simples possível.


Verificamos o comportamento correto testando e, portanto, concordo com Kent que a primeira e mais importante é que o software bem projetado deve passar nos testes. Vou ainda mais longe e insisto que o software deve ter testes (ou seja, ser bem coberto por testes).


Após a verificação do comportamento, os três pontos a seguir nas duas listas se relacionam à compreensão do nosso software (e, portanto, do seu comportamento). Sua lista começa com a duplicação de código, que realmente existe. No entanto, na minha experiência pessoal, focar demais na redução da duplicação de código é caro. Para eliminá-lo, é necessário criar abstrações que o ocultam e são essas abstrações que tornam o software difícil de entender e mudar.


A eliminação da duplicação de código requer abstrações, e as abstrações levam à complexidade


Não se repita ou DRY é usado para justificar decisões controversas de design. Você já viu código semelhante?


ZERO = BigDecimal.new(0) 

Além disso, você provavelmente viu algo assim:


 public void call(Map payload, boolean async, int errorStrategy) { // ... } 

Se você vir métodos ou funções com sinalizadores, booleanos etc., isso geralmente significa que alguém usou o princípio DRY ao refatorar, mas o código não era exatamente o mesmo nos dois lugares, portanto, o código resultante deveria ter ser flexível o suficiente para acomodar ambos os comportamentos.


Essas abstrações generalizadas são difíceis de testar e entender, pois devem lidar com muito mais casos do que o código original (possivelmente duplicado). Em outras palavras, as abstrações suportam muito mais comportamentos do que o necessário para o funcionamento normal do sistema. Assim, a eliminação da duplicação de código pode criar um novo comportamento que o sistema não exige.


Portanto, é realmente importante combinar alguns tipos de comportamento, mas pode ser difícil entender que tipo de comportamento é realmente duplicado. Muitas vezes, partes do código parecem semelhantes, mas isso acontece apenas por acidente.


Considere como é mais fácil eliminar a duplicação de código do que retorná-la novamente (por exemplo, depois de criar uma abstração mal pensada). Portanto, precisamos pensar em deixar um código duplicado, a menos que tenhamos certeza absoluta de que temos uma maneira melhor de nos livrarmos dele.


Criar abstrações deve nos fazer pensar. Se, no processo de eliminar o código duplicado, você criar uma abstração generalizada muito flexível, poderá ter seguido o caminho errado.


Isso nos leva ao próximo ponto - intenção versus comportamento.


A intenção do programador não tem sentido - o comportamento significa tudo


Muitas vezes elogiamos linguagens de programação, construções ou trechos de código por "revelar as intenções do programador". Mas qual é o sentido de conhecer intenções se você não pode prever o comportamento? E se você conhece comportamento, quanto significa intenção? Acontece que você precisa saber como o software deve se comportar, mas isso não é o mesmo que as "intenções do programador".


Vejamos este exemplo, que reflete muito bem as intenções do programador, mas não se comporta como pretendido:


 function LastModified(props) { return ( <div> Last modified on { props.date.toLocaleDateString() } </div> ); } 

Obviamente, o programador planejou que esse componente React exibisse uma data com a mensagem "Última modificação em". Isso funciona como pretendido? Na verdade não. E se this.prop.date não importar? Tudo simplesmente quebra. Não sabemos se foi concebido ou se alguém se esqueceu dele, e isso nem importa. O que importa é o comportamento.


E é exatamente isso que devemos saber se queremos mudar essa parte do código. Imagine que precisamos alterar a linha para "Última modificação". Embora possamos fazer isso, não está claro o que deve acontecer se a data estiver faltando. Seria melhor se escrevêssemos o componente de maneira a tornar seu comportamento mais compreensível.


 function LastModified(props) { if (!props.date) { throw "LastModified requires a date to be passed"; } return ( <div> Last modified on { props.date.toLocaleDateString() } </div> ); } 

Ou mesmo assim:


 function LastModified(props) { if (props.date) { return ( <div> Last modified on { props.date.toLocaleDateString() } </div> ); } else { return <div>Never modified</div>; } } 

Nos dois casos, o comportamento é mais compreensível e as intenções do programador não importam. Suponha que escolhemos a segunda alternativa (que lida com o valor da data ausente). Quando nos pedem para alterar a mensagem, podemos ver o comportamento e verificar se a mensagem "Nunca modificado" está correta ou se também precisa ser alterada.


Assim, quanto mais inequívoco for o comportamento , mais chances teremos de alterá-lo com sucesso. E isso significa que podemos precisar escrever mais código ou torná-lo mais preciso, ou até mesmo escrever código duplicado às vezes.


Isso também significa que precisaremos de mais classes, funções, métodos etc. É claro que gostaríamos de manter o número mínimo, mas não devemos usar esse número como métrica. Criar um grande número de classes ou métodos cria uma sobrecarga conceitual e mais conceitos aparecem no software do que unidades de modularidade. Portanto, precisamos reduzir o número de conceitos, o que, por sua vez, pode levar a uma diminuição no número de classes.


Os custos conceituais contribuem para confusão e complexidade


Para entender o que o código realmente fará, é necessário conhecer não apenas a área de assunto, mas também todos os conceitos usados ​​nesse código (por exemplo, ao procurar o desvio padrão, você deve conhecer a atribuição, adição, multiplicação, loops e comprimentos de matriz). Isso explica por que, à medida que o número de conceitos em um design aumenta, sua complexidade de entendimento aumenta.


Eu costumava escrever sobre despesas conceituais , e um bom efeito colateral de reduzir o número de conceitos em um sistema é que mais pessoas podem entender esse sistema. Isso, por sua vez, aumenta o número de pessoas que podem fazer alterações neste sistema. Definitivamente, um design de software que pode ser alterado com segurança por muitas pessoas é melhor do que um que só pode ser alterado por um pequeno punhado. (Portanto, acredito que a programação funcional hardcore nunca se tornará popular, pois requer uma compreensão profunda de muitos conceitos muito abstratos.)


A redução de custos conceituais reduzirá naturalmente o número de abstrações e facilitará a compreensão do comportamento. Não digo “nunca introduza um novo conceito”, digo que ele tem seu próprio preço e, se esse preço supera o benefício, a introdução de um novo conceito deve ser cuidadosamente considerada.


Quando escrevemos código ou projetamos software, devemos parar de pensar na elegância , beleza ou outra medida subjetiva de nosso código. Em vez disso, devemos sempre lembrar o que vamos fazer com o software.


Você não pendura o código na parede - você o altera


Um código não é uma obra de arte que você pode imprimir e pendurar em um museu. O código está em execução. É estudado e depurado. E, o mais importante, está mudando . E frequentemente. Qualquer projeto difícil de trabalhar deve ser questionado e revisado. Qualquer design que reduz o número de pessoas que podem trabalhar com ele também deve ser questionado.


O código deve funcionar, portanto deve ser testado. O código possui bugs e exigirá a adição de novos recursos, portanto, precisamos entender seu comportamento. O código vive mais do que a capacidade de um programador específico para suportá-lo, portanto, devemos nos esforçar por um código que seja compreensível para uma ampla gama de pessoas.


Ao escrever seu código ou projetar seu sistema, você simplifica a explicação do comportamento do sistema? Torna-se mais fácil entender como ela se comportará? Você está focado em resolver o problema bem na sua frente ou em um problema mais abstrato?


Sempre tente manter o comportamento simples para demonstração, predição e compreensão, e mantenha o número de conceitos no mínimo absoluto.

Source: https://habr.com/ru/post/pt461823/


All Articles