¿Cómo escribir código que todos entiendan?


De un traductor: publicamos un artículo de Camil Lelonek para usted sobre el significado de la legibilidad del código y la "empatía de los programadores".

¿Alguna vez te has preguntado quién verá tu código? ¿Qué tan difícil puede ser para los demás? ¿Intentó determinar su legibilidad?
“Cualquier tonto puede escribir código que la máquina entiende. Pero solo los buenos programadores escriben código que la gente también entiende ”, dice Martin Fowler.
De vez en cuando, cuando veo algunos fragmentos de código, pierdo la fe en la existencia de empatía entre los programadores. Debes entender de lo que estoy hablando, porque cada uno de nosotros nos encontramos con un código que fue escrito terriblemente y era prácticamente ilegible.
Skillbox recomienda: Curso práctico de dos años "Soy un desarrollador web PRO" .

Le recordamos: para todos los lectores de "Habr": un descuento de 10.000 rublos al registrarse en cualquier curso de Skillbox con el código de promoción "Habr".
Hace poco vi algo como esto:

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

En principio, todo está bien aquí: tal vez alguien simplemente tiene una fantasía o el autor del código tiene una sólida base matemática. No quería volver a escribir este código, pero inconscientemente me pareció que algo andaba mal aquí. “Debe haber una manera de mejorarlo, formularlo de manera diferente. Veré cómo funciona todo ", pensé. Bastante rápido, encontré esto:

 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 no solo se está importando ~>, sino también las funciones conn! / 0 y do_reset! / 1. Ok, echemos un vistazo al módulo Restablecer:

 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 

En cuanto a conn!, Hay un par de formas de facilitar este sitio. Sin embargo, detenerse en este punto no tiene sentido. Prefiero centrarme en do_reset! / 1. Esta función devuelve una función que devuelve un argumento y realiza un reinicio para el Inicializador; y el nombre en sí es bastante complicado.



Decidí aplicar ingeniería inversa al código. De acuerdo con la documentación del beneficiario, before_scenario toma la entrada del script como argumento. El valor de retorno se convierte en la entrada para los próximos pasos. Esto es lo que probablemente quiso decir el autor:

  • Inicializando una conexión Postgrex.
  • Restablecer EventStore.
  • Usar valores de entrada como elementos de configuración (hablando sobre el número de cuentas).
  • Preparar datos para pruebas (es decir, crear usuarios e ingresar a la aplicación).
  • Utilizando puntos de referencia.

En general, todo está claro, dicho código es fácil de escribir. Observo que al refactorizar no mostraré ni modificaré la función init, esto no es muy importante aquí.

El primer paso es usar explícitamente alias en lugar de importaciones implícitas. Nunca me gustaron las funciones "mágicas" que aparecen en mi código, a pesar de que Ecto.Query hace que las consultas sean elegantes. Ahora nuestro módulo de conexión se ve así:

 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 

Luego, decidí escribir un "gancho", como se sugiere en la documentación:

before_scenario: entradas fn -> entradas finales

Todo lo que queda por hacer es preparar los datos. El resultado final es el siguiente:

 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) 

¿Es este código perfecto? Probablemente aún no. ¿Pero es más fácil de entender? Eso espero. ¿Podría hacerse de inmediato? Definitivamente si.

Cual es el problema


Cuando le propuse una solución al autor, escuché: "Genial". Sin embargo, no esperaba más.

El problema es que el código primario funcionó. Lo único que me llevó a pensar en la necesidad de refactorizar fue la estructura demasiado compleja del código y su baja legibilidad.

Para convencer a otros desarrolladores de hacer que su código sea legible, necesita algo convincente. Y el argumento "Decidí rehacer su código, porque es oscuro", no será aceptado, la respuesta es "significa que usted es simplemente un mal desarrollador, qué puedo hacer ¯ \ _ (ツ) _ / ¯".



Este es un problema (no) de gestión


Nadie se sorprende de que el negocio espere resultados de los empleados. Y cuanto antes se reciban, mejor. Los gerentes generalmente evalúan el software y su redacción en términos de plazos, presupuesto y velocidad. No digo que esto sea malo, solo estoy tratando de explicar por qué no hay un código de alta calidad que "simplemente funcione". El hecho es que los gerentes no están muy interesados ​​en la belleza y la legibilidad, necesitan ventas, bajos costos y resultados rápidos.

Cuando los programadores están presionados, buscan una salida. La mayoría de las veces, la solución es crear un "código de trabajo" en el que puede haber un montón de "muletas". Se crea sin pensar que el código debe mantenerse en el futuro. Y el código elegante es muy difícil de escribir rápidamente. No importa cuán experimentado sea el programador, cuando el trabajo se realiza bajo presión de tiempo, nadie piensa en la belleza.

Esta situación solo puede resolverse convenciendo a los gerentes de que un código incorrecto (aunque funciona) amenaza con aumentar el costo de su mantenimiento en el futuro cercano. Corregir errores y agregar nuevas características, revisar, escribir documentación técnica: en el caso de un código de baja calidad, todo esto lleva mucho más tiempo que en una situación en la que es elegante y racional.



El importante papel de la empatía.


Si está desarrollando software, entonces comprende que está destinado a otras personas, y el desarrollo se lleva a cabo en equipo. Y la empatía es muy importante aquí.

Su código es una forma peculiar de comunicación. En el proceso de desarrollo de la arquitectura de software futuro, debe pensar en aquellos que interactuarán con su código.

La "empatía de los programadores" ayuda a crear un código más limpio y racional, incluso cuando se acaban los plazos, y el gerente está constantemente "aplastado". Ayuda a comprender cómo es analizar el código ilegible de otra persona, lo cual es extremadamente difícil de entender.



Como conclusión


Recientemente escribí código en Elixir:

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

Luego pensé en un método de toque Ruby que te permite reescribir este código:

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

En mi humilde opinión, sería mejor hacer esto sin el resultado de la variable intermedia. Pensé en cómo se podría hacer esto y llegué a la siguiente conclusión:

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

El resultado será el mismo. Pero usar con puede causar problemas a alguien que estudie este código, ya que en el caso habitual, con se usa de manera diferente.

Por lo tanto, decidí dejar todo como estaba, para no hacerlo mejor a expensas de los demás. Le pido que haga lo mismo, sin complicar el código más allá de toda medida. Antes de introducir alguna función o variable exótica, recuerde que sus colegas pueden simplemente no entenderlas en una revisión.

En general, recomiendo usar el siguiente principio: "Cuando escriba SU código, piense en OTRO".

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


All Articles