Hola Habr! Les presento el artículo "Cuatro mejores reglas para el diseño de software" de David Bryant Copeland. David Bryant Copeland es arquitecto de software y CTO de Stitch Fix. Mantiene un blog y es autor de varios libros .
Martin Fowler tuiteó recientemente con un enlace a su publicación de blog sobre cuatro reglas de diseño simples de Kent Beck, que creo que pueden mejorarse aún más (y que a veces pueden enviar al programador por el camino equivocado):
Las reglas de Kent de la programación extrema explicadas :
- Kent dice: "Ejecute todas las pruebas".
- No duplicar la lógica. Intente evitar duplicados ocultos, como las jerarquías de clases paralelas.
- Todas las intenciones importantes para el programador deben ser claramente visibles.
- El código debe tener el menor número posible de clases y métodos.
Según mi experiencia, estas reglas no satisfacen las necesidades del diseño de software. Mis cuatro reglas para un sistema bien diseñado pueden ser:
- está bien cubierto por las pruebas y las supera con éxito.
- no tiene abstracciones que el programa no necesita directamente.
- ella tiene un comportamiento inequívoco.
- requiere la menor cantidad de conceptos.
Para mí, estas reglas se derivan de lo que hacemos con nuestro software.
Entonces, ¿qué hacemos con nuestro software?
No podemos hablar sobre diseño de software sin antes hablar sobre lo que pretendemos hacer con él.
El software está escrito para resolver el problema. El programa se ejecuta y tiene un comportamiento. Este comportamiento se estudia para garantizar un funcionamiento correcto o para detectar errores. El software también a menudo cambia para darle un comportamiento nuevo o modificado.
Por lo tanto, cualquier enfoque para el diseño de software debe centrarse en predecir, estudiar y comprender su comportamiento para hacer que cambiar este comportamiento sea lo más simple posible.
Verificamos la corrección del comportamiento mediante pruebas y, por lo tanto, estoy de acuerdo con Kent en que lo primero y más importante es que un software bien diseñado debe pasar las pruebas. Incluso iré más allá e insistiré en que el software debe tener pruebas (es decir, estar bien cubierto por las pruebas).
Después de que se haya verificado el comportamiento, los siguientes tres puntos en ambas listas se relacionan con la comprensión de nuestro software (y, por lo tanto, su comportamiento). Su lista comienza con la duplicación de código, que realmente está en su lugar. Sin embargo, en mi experiencia personal, centrarme demasiado en reducir la duplicación de código es costoso. Para eliminarlo, es necesario crear abstracciones que lo oculten, y son estas abstracciones las que hacen que el software sea difícil de entender y cambiar.
Eliminar la duplicación de código requiere abstracciones, y las abstracciones conducen a la complejidad.
Don't Repeat Yourself o DRY se usa para justificar decisiones de diseño controvertidas. ¿Alguna vez has visto un código similar?
ZERO = BigDecimal.new(0)
Además, probablemente viste algo como esto:
public void call(Map payload, boolean async, int errorStrategy) {
Si ve métodos o funciones con banderas, booleanos, etc., esto generalmente significa que alguien usó el principio DRY al refactorizar, pero el código no era exactamente el mismo en ambos lugares, por lo que el código resultante debería tener ser lo suficientemente flexible como para acomodar ambos comportamientos.
Tales abstracciones generalizadas son difíciles de probar y comprender, ya que deberían manejar muchos más casos que el código original (posiblemente duplicado). En otras palabras, las abstracciones soportan muchos más comportamientos de los necesarios para el funcionamiento normal del sistema. Por lo tanto, eliminar la duplicación de código puede crear un nuevo comportamiento que el sistema no requiere.
Por lo tanto, es realmente importante combinar algunos tipos de comportamiento, pero puede ser difícil entender qué tipo de comportamiento se duplica realmente. A menudo, las piezas de código se ven similares, pero esto sucede solo por accidente.
Considere cuánto más fácil es eliminar la duplicación de código que devolverla nuevamente (por ejemplo, después de crear una abstracción mal pensada). Por lo tanto, debemos pensar en dejar código duplicado, a menos que estemos absolutamente seguros de que tenemos una mejor manera de deshacernos de él.
Crear abstracciones debería hacernos pensar. Si en el proceso de eliminar el código duplicado crea una abstracción generalizada muy flexible, es posible que haya tomado el camino equivocado.
Esto nos lleva al siguiente punto: intención versus comportamiento.
La intención del programador no tiene sentido: el comportamiento lo es todo
A menudo elogiamos los lenguajes de programación, construcciones o fragmentos de código por "revelar las intenciones del programador". Pero, ¿qué sentido tiene conocer las intenciones si no puede predecir el comportamiento? Y si conoces el comportamiento, ¿cuánto significa la intención? Resulta que necesita saber cómo debe comportarse el software, pero esto no es lo mismo que las "intenciones del programador".
Veamos este ejemplo, que refleja muy bien las intenciones del programador, pero no se comporta según lo previsto:
function LastModified(props) { return ( <div> Last modified on { props.date.toLocaleDateString() } </div> ); }
Obviamente, el programador planeó que este componente React mostrara una fecha con el mensaje "Última modificación el". ¿Funciona esto según lo previsto? En realidad no ¿Qué pasa si this.prop.date no importa? Todo simplemente se rompe. No sabemos si fue tan concebido, o si alguien simplemente lo olvidó, y eso ni siquiera importa. Lo que importa es el comportamiento.
Y esto es exactamente lo que debemos saber si queremos cambiar esta parte del código. Imagina que necesitamos cambiar la línea a "Última modificación". Aunque podemos hacer esto, no está claro qué debería suceder si falta la fecha. Sería mejor si en su lugar escribiéramos el componente de tal manera que su comportamiento sea más comprensible.
function LastModified(props) { if (!props.date) { throw "LastModified requires a date to be passed"; } return ( <div> Last modified on { props.date.toLocaleDateString() } </div> ); }
O incluso así:
function LastModified(props) { if (props.date) { return ( <div> Last modified on { props.date.toLocaleDateString() } </div> ); } else { return <div>Never modified</div>; } }
En ambos casos, el comportamiento es más comprensible y las intenciones del programador no importan. Supongamos que elegimos la segunda alternativa (que maneja el valor de la fecha que falta). Cuando se nos pide que cambiemos el mensaje, podemos ver el comportamiento y verificar si el mensaje "Nunca modificado" es correcto o si también necesita ser cambiado.
Por lo tanto, cuanto más inequívoco sea el comportamiento , más posibilidades tenemos de cambiarlo con éxito. Y esto significa que es posible que necesitemos escribir más código o hacerlo más preciso, o incluso escribir código duplicado a veces.
También significa que necesitaremos más clases, funciones, métodos, etc. Por supuesto, nos gustaría mantener su número mínimo, pero no debemos usar este número como nuestra métrica. La creación de una gran cantidad de clases o métodos crea una sobrecarga conceptual , y aparecen más conceptos en el software que unidades de modularidad. Por lo tanto, necesitamos reducir el número de conceptos, lo que, a su vez, puede conducir a una disminución en el número de clases.
Los costos conceptuales contribuyen a la confusión y la complejidad.
Para comprender qué hará realmente el código, debe conocer no solo el área temática, sino también todos los conceptos utilizados en este código (por ejemplo, al buscar la desviación estándar, debe conocer la asignación, la suma, la multiplicación, los bucles y las longitudes de la matriz). Esto explica por qué a medida que aumenta el número de conceptos en un diseño, aumenta su complejidad para la comprensión.
Solía escribir sobre gastos conceptuales , y un buen efecto secundario de reducir la cantidad de conceptos en un sistema es que más personas pueden entender este sistema. Esto a su vez aumenta el número de personas que pueden realizar cambios en este sistema. Definitivamente, un diseño de software que muchas personas pueden cambiar de forma segura es mejor que uno que solo un pequeño puñado pueda cambiar. (Por lo tanto, creo que la programación funcional hardcore nunca será popular, ya que requiere una comprensión profunda de muchos conceptos muy abstractos).
Reducir los costos conceptuales naturalmente reducirá la cantidad de abstracciones y hará que el comportamiento sea más fácil de entender. No digo "nunca introducir un nuevo concepto", digo que tiene su propio precio, y si este precio supera el beneficio, la introducción de un nuevo concepto debe considerarse cuidadosamente.
Cuando escribimos código o diseñamos software, debemos dejar de pensar en la elegancia , belleza u otra medida subjetiva de nuestro código. En cambio, siempre debemos recordar lo que vamos a hacer con el software.
No cuelgas el código en la pared, lo cambias
Un código no es una obra de arte que puede imprimir y colgar en un museo. El código se está ejecutando. Es estudiado y depurado. Y, lo más importante, está cambiando . Y a menudo Cualquier diseño con el que sea difícil trabajar debe ser cuestionado y revisado. Cualquier diseño que reduzca el número de personas que pueden trabajar con él también debe ser cuestionado.
El código debería funcionar, por lo que debería ser probado. El código tiene errores y requerirá la adición de nuevas características, por lo que debemos comprender su comportamiento. El código dura más que la capacidad de un programador en particular para admitirlo, por lo que debemos luchar por un código que sea comprensible para una amplia gama de personas.
Cuando escribe su código o diseña su sistema, ¿simplifica la explicación del comportamiento del sistema? ¿Se vuelve más fácil entender cómo se comportará? ¿Estás enfocado en resolver el problema justo en frente tuyo o en uno más abstracto?
Siempre trate de mantener el comportamiento simple para la demostración, predicción y comprensión, y mantenga el número de conceptos al mínimo absoluto.