Los seis mandamientos del buen código: escribir código que supere la prueba del tiempo
Publicado: 2022-03-11Los humanos solo han estado lidiando con el arte y la ciencia de la programación de computadoras durante aproximadamente medio siglo. En comparación con la mayoría de las artes y las ciencias, la informática es, en muchos sentidos, todavía un niño pequeño, que camina contra las paredes, se tropieza con sus propios pies y, en ocasiones, arroja comida sobre la mesa.
Como consecuencia de su relativa juventud, no creo que tengamos un consenso todavía sobre cuál es una definición adecuada de "buen código", ya que esa definición continúa evolucionando. Algunos dirán que un "buen código" es un código con una cobertura de prueba del 100 %. Otros dirán que es súper rápido y tiene un rendimiento increíble y funcionará aceptablemente en hardware de 10 años.
Si bien todos estos son objetivos loables para los desarrolladores de software, sin embargo, me aventuro a incluir otro objetivo en la mezcla: la mantenibilidad. Específicamente, el "buen código" es un código que una organización puede mantener fácil y rápidamente (¡no solo su autor!) y vivirá por más tiempo que el sprint en el que fue escrito. carrera como ingeniero en empresas grandes y pequeñas, en los EE. UU. y en el extranjero, que parecen correlacionarse con un software "bueno" y mantenible.
Mandamiento n.º 1: trate su código de la forma en que desea que el código de los demás lo trate a usted
Estoy lejos de ser la primera persona en escribir que la audiencia principal de su código no es el compilador/computadora, sino el próximo que tiene que leer, comprender, mantener y mejorar el código (que no será necesariamente usted dentro de seis meses). ). Cualquier ingeniero que valga la pena puede producir código que "funciona"; lo que distingue a un excelente ingeniero es que puede escribir código mantenible de manera eficiente que respalda un negocio a largo plazo y tiene la habilidad para resolver problemas de manera simple, clara y mantenible.
En cualquier lenguaje de programación, es posible escribir código bueno o código malo. Suponiendo que juzguemos un lenguaje de programación por lo bien que facilita la escritura de un buen código (al menos debería ser uno de los criterios principales, de todos modos), cualquier lenguaje de programación puede ser "bueno" o "malo" dependiendo de cómo se use (o se abuse de él). ).
Un ejemplo de un lenguaje que muchos consideran “limpio” y legible es Python. El lenguaje en sí impone cierto nivel de disciplina de espacios en blanco y las API integradas son abundantes y bastante consistentes. Dicho esto, es posible crear monstruos indescriptibles. Por ejemplo, se puede definir una clase y definir/redefinir/anular la definición de todos y cada uno de los métodos de esa clase durante el tiempo de ejecución (a menudo denominado parcheado mono). Esta técnica conduce naturalmente, en el mejor de los casos, a una API inconsistente y, en el peor de los casos, a un monstruo imposible de depurar. Uno podría pensar ingenuamente, "claro, ¡pero nadie hace eso!" Desafortunadamente, lo hacen, y no toma mucho tiempo navegar por pypi antes de encontrarse con bibliotecas sustanciales (¡y populares!) que (ab)utilizan parches de mono ampliamente como el núcleo de sus API. Recientemente utilicé una biblioteca de redes cuya API cambia completamente según el estado de la red de un objeto. Imagine, por ejemplo, llamar a client.connect() y, a veces, obtener un error MethodDoesNotExist en lugar de HostNotFound o NetworkUnavailable .
Mandamiento n.° 2: el buen código se lee y comprende fácilmente, en parte y en su totalidad
El buen código es fácil de leer y entender, en parte y en su totalidad, por otros (así como por el autor en el futuro, tratando de evitar el síndrome de "¿Realmente escribí eso?" ).
Por "en parte" quiero decir que, si abro algún módulo o función en el código, debería poder entender lo que hace sin tener que leer también el resto del código base. Debe ser lo más intuitivo y autodocumentado posible.
El código que constantemente hace referencia a detalles minuciosos que afectan el comportamiento de otras partes (aparentemente irrelevantes) de la base de código es como leer un libro en el que tiene que hacer referencia a las notas al pie o un apéndice al final de cada oración. ¡Nunca pasarías de la primera página!
Algunas otras ideas sobre la legibilidad "local":
El código bien encapsulado tiende a ser más legible, separando las preocupaciones en todos los niveles.
Los nombres importan. Active el sistema Thinking Fast and Slow 2 en el que el cerebro forma pensamientos y pone un pensamiento real y cuidadoso en nombres de métodos y variables. Los pocos segundos extra pueden pagar importantes dividendos. Una variable bien nombrada puede hacer que el código sea mucho más intuitivo, mientras que una variable mal nombrada puede generar falsedades y confusión.
La astucia es el enemigo. Cuando use técnicas, paradigmas u operaciones sofisticados (como listas de comprensión u operadores ternarios), tenga cuidado de usarlos de una manera que haga que su código sea más legible, no solo más corto.
La consistencia es algo bueno. La consistencia en el estilo, tanto en términos de cómo se colocan las llaves como en términos de operaciones, mejora enormemente la legibilidad.
Separación de intereses. Un proyecto determinado gestiona un número innumerable de suposiciones importantes a nivel local en varios puntos de la base de código. Exponga cada parte de la base de código a la menor cantidad posible de esas preocupaciones. Supongamos que tiene un sistema de gestión de personas en el que un objeto de persona a veces puede tener un apellido nulo. Para alguien que escribe código en una página que muestra objetos personales, ¡eso podría ser realmente incómodo! Y a menos que mantenga un manual de "suposiciones incómodas y no obvias que tiene nuestra base de código" (sé que no lo hago), el programador de su página de visualización no sabrá que los apellidos pueden ser nulos y probablemente escribirá código con un puntero nulo excepción cuando aparece el caso nulo del apellido. En su lugar, maneje estos casos con API y contratos bien pensados que las diferentes partes de su base de código usan para interactuar entre sí.
Mandamiento n.º 3: el buen código tiene un diseño y una arquitectura bien pensados para que la gestión del estado sea obvia
El Estado es el enemigo. ¿Por qué? Porque es la parte más compleja de cualquier aplicación y debe tratarse de manera muy deliberada y cuidadosa. Los problemas comunes incluyen inconsistencias en la base de datos, actualizaciones parciales de la interfaz de usuario donde los nuevos datos no se reflejan en todas partes, operaciones fuera de orden o simplemente un código increíblemente complejo con declaraciones if y ramas en todas partes que conducen a un código difícil de leer e incluso más difícil de mantener. Poner el estado en un pedestal para ser tratado con mucho cuidado, y ser extremadamente consistente y deliberado con respecto a cómo se accede y modifica el estado, simplifica drásticamente su base de código. Algunos lenguajes (Haskell, por ejemplo) imponen esto a nivel programático y sintáctico. Se sorprendería de cuánto puede mejorar la claridad de su base de código si tiene bibliotecas de funciones puras que no acceden a ningún estado externo, y luego una pequeña superficie de código con estado que hace referencia a la funcionalidad pura exterior.

Mandamiento #4: El buen código no reinventa la rueda, se sostiene sobre los hombros de los gigantes
Antes de reinventar potencialmente una rueda, piense qué tan común es el problema que está tratando de resolver o la función que está tratando de realizar. Es posible que alguien ya haya implementado una solución que pueda aprovechar. Tómese el tiempo para pensar e investigar tales opciones, si es apropiado y está disponible.
Dicho esto, un contraargumento completamente razonable es que las dependencias no vienen “gratis” sin ningún inconveniente. Al utilizar una biblioteca de código abierto o de terceros que agrega alguna funcionalidad interesante, se está comprometiendo con esa biblioteca y se está volviendo dependiente de ella. Ese es un gran compromiso; si es una biblioteca gigante y solo necesita un poco de funcionalidad, ¿realmente quiere la carga de actualizar toda la biblioteca si actualiza, por ejemplo, a Python 3.x? Y además, si encuentra un error o desea mejorar la funcionalidad, depende del autor (o proveedor) para proporcionar la corrección o mejora o, si es de código abierto, se encuentra en la posición de explorar un ( potencialmente sustancial) base de código con la que no está familiarizado para tratar de arreglar o modificar un poco oscuro de funcionalidad.
Ciertamente, cuanto mejor se use el código del que depende, menos probable es que tenga que invertir tiempo en el mantenimiento. La conclusión es que vale la pena que haga su propia investigación y haga su propia evaluación de si incluir o no tecnología externa y cuánto mantenimiento agregará esa tecnología en particular a su pila.
A continuación se presentan algunos de los ejemplos más comunes de cosas que probablemente no debería reinventar en la era moderna en su proyecto (a menos que estos SON sus proyectos).
bases de datos
Averigüe cuál de los CAP necesita para su proyecto, luego elija la base de datos con las propiedades correctas. La base de datos ya no significa solo MySQL, puede elegir entre:
- Schema'ed SQL "tradicional": Postgres / MySQL / MariaDB / MemSQL / Amazon RDS, etc.
- Tiendas de valor clave: Redis / Memcache / Riak
- NoSQL: MongoDB/Cassandra
- Bases de datos alojadas : AWS RDS / DynamoDB / AppEngine Datastore
- Trabajo pesado: Amazon MR / Hadoop (Hive/Pig) / Cloudera / Google Big Query
- Cosas locas: Mnesia de Erlang, Core Data de iOS
Capas de abstracción de datos
En la mayoría de las circunstancias, no debe escribir consultas sin procesar en cualquier base de datos que elija usar. Lo más probable es que exista una biblioteca para ubicarse entre la base de datos y el código de su aplicación, separando las preocupaciones de administrar sesiones de base de datos simultáneas y los detalles del esquema de su código principal. Como mínimo, nunca debe tener consultas sin formato o SQL en línea en medio del código de su aplicación. Más bien, envuélvalo en una función y centralice todas las funciones en un archivo llamado algo realmente obvio (por ejemplo, "consultas.py"). Una línea como users = load_users() , por ejemplo, es infinitamente más fácil de leer que users = db.query(“SELECT username, foo, bar from users LIMIT 10 ORDER BY ID”) . Este tipo de centralización también hace que sea mucho más fácil tener un estilo consistente en sus consultas y limita la cantidad de lugares a los que ir para cambiar las consultas en caso de que cambie el esquema.
Otras bibliotecas y herramientas comunes para considerar aprovechar
- Servicios de colas o Pub/Sub. Elija entre los proveedores de AMQP, ZeroMQ, RabbitMQ, Amazon SQS
- Almacenamiento. Amazon S3, almacenamiento en la nube de Google
- Monitoreo: Grafito/Grafito alojado, AWS Cloud Watch, New Relic
- Recopilación/Agregación de registros. Loggly, Splunk
Escalado automático
- Escalado automático. Heroku, AWS Beanstalk, AppEngine, AWS Opsworks, Océano digital
Mandamiento #5: ¡No cruces los arroyos!
Hay muchos buenos modelos para el diseño de programación, pub/sub, actores, MVC, etc. Elige el que más te guste y apégate a él. Los diferentes tipos de lógica que se ocupan de diferentes tipos de datos deben aislarse físicamente en la base de código (nuevamente, esta separación de preocupaciones es concepto y reduce la carga cognitiva en el futuro lector). El código que actualiza su interfaz de usuario debe ser físicamente distinto del código que calcula lo que entra en la interfaz de usuario, por ejemplo.
Mandamiento #6: Cuando sea posible, deje que la computadora haga el trabajo
Si el compilador puede detectar errores lógicos en su código y evitar el mal comportamiento, los errores o los bloqueos absolutos, deberíamos aprovecharlo. Por supuesto, algunos lenguajes tienen compiladores que lo hacen más fácil que otros. Haskell, por ejemplo, tiene un famoso compilador estricto que hace que los programadores dediquen la mayor parte de su esfuerzo a obtener código para compilar. Sin embargo, una vez que se compila, "simplemente funciona". Para aquellos de ustedes que nunca han escrito en un lenguaje funcional fuertemente tipado, esto puede parecer ridículo o imposible, pero no confíen en mi palabra. En serio, haga clic en algunos de estos enlaces, es absolutamente posible vivir en un mundo sin errores de tiempo de ejecución. Y realmente es así de mágico.
Es cierto que no todos los lenguajes tienen un compilador o una sintaxis que se presta a muchas (¡o en algunos casos a ninguna!) verificación en tiempo de compilación. Para aquellos que no lo hacen, tómese unos minutos para investigar qué controles de rigor opcionales puede habilitar en su proyecto y evaluar si tienen sentido para usted. Una lista breve y no exhaustiva de algunos de los más comunes que he usado últimamente para lenguajes con tiempos de ejecución indulgentes incluyen:
- Python: pylint, pyflakes, advertencias, advertencias en emacs
- Rubí: advertencias
- JavaScript: jslint
Conclusión
Esta no es una lista exhaustiva o perfecta de mandamientos para producir código "bueno" (es decir, fácil de mantener). Dicho esto, si cada código base que tuve que aprender en el futuro siguiera aunque sea la mitad de los conceptos de esta lista, tendré muchas menos canas e incluso podría agregar cinco años adicionales al final de mi vida. Y ciertamente encontraré el trabajo más agradable y menos estresante.
