Como escrever um código que todos entenderão?


De um tradutor: Publicamos para você um artigo de Camil Lelonek sobre o significado da legibilidade do código e a “empatia dos programadores”.

Você já se perguntou quem visualizará seu código? Quão difícil pode ser para os outros? Tentou determinar sua legibilidade?
“Qualquer idiota pode escrever um código que a máquina entenda. Mas apenas bons programadores escrevem código que as pessoas também entendem ”, diz Martin Fowler.
De tempos em tempos, quando vejo alguns trechos de código, perco a fé na existência de empatia entre os programadores. Você deve entender do que estou falando - porque cada um de nós se deparou com um código que foi escrito demais e praticamente ilegível.
A Skillbox recomenda: Curso prático de dois anos "Eu sou um desenvolvedor Web PRO" .

Lembramos que: para todos os leitores de "Habr" - um desconto de 10.000 rublos ao se inscrever em qualquer curso Skillbox usando o código promocional "Habr".
Vi recentemente algo assim:

defmodule Util.Combinators do def then(a, b) do fn data -> b.(a.(data)) end end def a ~> b, do: a |> then(b) end 

Em princípio, está tudo bem aqui: talvez alguém tenha apenas uma fantasia ou o autor do código tenha uma sólida formação matemática. Eu não queria reescrever esse código, mas subconscientemente me pareceu que algo estava errado aqui. “Deve haver uma maneira de torná-lo melhor, de formular de maneira diferente. Vou ver como tudo funciona ", pensei. Muito rapidamente, encontrei o seguinte:

 import Util.{Reset, Combinators} # ... conn = conn!() Benchee.run( # ... time: 40, warmup: 10, inputs: inputs, before_scenario: do_reset!(conn) ~> init, formatter_options: %{console: %{extended_statistics: true}} ) 

Hmmm, parece que não apenas está sendo importado ~>, mas também as funções conn! / 0 e do_reset! / 1. Ok, vamos dar uma olhada no módulo Reset:

 defmodule Util.Reset do alias EventStore.{Config, Storage.Initializer} def conn! do {:ok, conn} = Config.parsed() |> Config.default_postgrex_opts() |> Postgrex.start_link() conn end def do_reset!(conn) do fn data -> Initializer.reset!(conn) data end end end 

Quanto ao conn!, Existem algumas maneiras de tornar este site mais fácil. No entanto, parar neste momento não faz sentido. Prefiro me concentrar em do_reset! / 1. Essa função retorna uma função que retorna um argumento e executa uma redefinição para o Inicializador; e o nome em si é bastante complicado.



Eu decidi fazer engenharia reversa do código. De acordo com a documentação do benchee, before_scenario recebe a entrada do script como argumento. O valor de retorno se torna a entrada para as próximas etapas. Aqui está o que o autor provavelmente quis dizer:

  • Inicializando uma conexão Postgrex.
  • Redefinir EventStore.
  • Usando valores de entrada como itens de configuração (falando sobre o número de contas).
  • Preparando dados para testes (ou seja, criando usuários e inserindo o aplicativo).
  • Usando benchmarks.

Em geral, tudo está claro, esse código é fácil de escrever. Observo que na refatoração não mostrarei ou modificarei a função init, isso não é muito importante aqui.

A primeira etapa é usar explicitamente o alias em vez de importações implícitas. Eu nunca gostei dos recursos "mágicos" que aparecem no meu código, embora o Ecto.Query torne as consultas elegantes. Agora, nosso módulo de conexão se parece com isso:

 defmodule Benchmarks.Util.Connection do alias EventStore.{Config, Storage.Initializer} def init! do with {:ok, conn} = Config.parsed() |> Config.default_postgrex_opts() |> Postgrex.start_link() do conn end end def reset!(conn), do: Initializer.reset!(conn) end 

Em seguida, decidi escrever um "gancho", conforme sugerido na documentação:

before_scenario: fn inputs -> inputs end

Tudo o que falta fazer é preparar os dados. O resultado final é o seguinte:

 alias Benchmarks.Util.Connection conn = Connection.init!() # ... Benchee.run( inputs: inputs, before_scenario: fn inputs -> Connection.reset!(conn) init.(inputs) end, formatter_options: %{console: %{extended_statistics: true}} ) Connection.reset!(conn) 

Esse código é perfeito? Provavelmente ainda não. Mas é mais fácil entender? Espero que sim. Isso poderia ser feito imediatamente? Definitivamente sim.

Qual é o problema?


Quando propus uma solução ao autor, ouvi: "Legal". No entanto, eu não esperava mais.

O problema é que o código principal funcionou. A única coisa que me levou a pensar sobre a necessidade de refatoração foi a estrutura muito complexa do código e sua baixa legibilidade.

Para convencer outros desenvolvedores a tornar seu código legível, você precisa de algo convincente. E o argumento “Decidi refazer seu código, porque é obscuro”, não será aceito, a resposta é “significa que você é apenas um desenvolvedor ruim, o que posso fazer ¯ \ _ (ツ) _ / ¯”.



Esse é um problema (não) de gerenciamento


Ninguém fica surpreso que o negócio espere resultados dos funcionários. E quanto mais cedo eles forem recebidos, melhor. Os gerentes geralmente avaliam o software e sua redação em termos de prazos, orçamento e velocidade. Não estou dizendo que isso é ruim, estou apenas tentando explicar por que não há código de alta qualidade que "simplesmente funcione". O fato é que os gerentes não estão muito interessados ​​em beleza e legibilidade, precisam de vendas, baixos custos e resultados rápidos.

Quando os programadores são pressionados, eles procuram uma saída. Na maioria das vezes, a solução é criar um "código de trabalho" no qual possa haver várias "muletas". Ele é criado sem o pensamento de que o código precisa ser mantido no futuro. E código elegante é muito difícil de escrever rapidamente. Não importa a experiência do programador, quando o trabalho é feito sob pressão do tempo, ninguém pensa em beleza.

Essa situação pode ser resolvida apenas convencendo os gerentes de que um código ruim (embora funcionando) ameaça aumentar o custo de sua manutenção em um futuro próximo. Corrigindo bugs e adicionando novos recursos, revisando e escrevendo documentação técnica - no caso de código de baixa qualidade, tudo isso leva muito mais tempo do que em uma situação em que é elegante e racional.



O importante papel da empatia


Se você está desenvolvendo software, entende que ele é destinado a outras pessoas e o desenvolvimento é realizado em equipe. E a empatia é muito importante aqui.

Seu código é uma forma peculiar de comunicação. No processo de desenvolvimento da arquitetura de software futuro, você precisa pensar sobre quem irá interagir com seu código.

A "empatia dos programadores" ajuda a criar um código mais limpo e racional, mesmo quando os prazos estão acabando, e o gerente está constantemente "pressionando". Ajuda a entender como é analisar o código ilegível de outra pessoa, o que é extremamente difícil de entender.



Como conclusão


Eu escrevi recentemente um código no Elixir:

 result = calculate_results() Connection.close(conn) result 

Então, pensei em um método de torneira Ruby que permite reescrever esse código:

 calculate_result().tap do Connection.close(conn) end 

IMHO, seria melhor fazer isso sem o resultado da variável intermediária. Pensei em como isso poderia ser feito e cheguei à seguinte conclusão:

 with result = calculate_results(), Connection.close(conn), do: result 

O resultado será o mesmo. Mas usar com pode causar problemas para alguém que estuda esse código, pois, no caso usual, com é usado de maneira diferente.

Por isso, decidi deixar tudo como estava, para não me sair melhor às custas dos outros. Peço que você faça o mesmo, sem complicar o código além da medida. Antes de introduzir alguma função ou variável exótica, lembre-se de que seus colegas podem simplesmente não entendê-las em uma revisão.

Em geral, recomendo usar o seguinte princípio: "Ao escrever SEU código, pense em OUTRO".

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


All Articles