Buenas y malas prácticas de software pasadas y presentes

Qué hace que el software robusto sea tan especial y por qué los desarrolladores (y las empresas) lo necesitan.

Documento técnico del Dr. Bernhard Scheffold, OXID eSales AG

Una parte esencial en el desarrollo de una solución OXID eShop para una empresa mediana es la integración de la tienda con los sistemas que la rodean. Esto incluye sistemas de planificación de recursos empresariales, sistemas de gestión de información de productos, sistemas CRM y servidores de búsqueda externos. Estos últimos son a menudo una necesidad para realizar un servicio de búsqueda eficiente.

Si el servicio de búsqueda es una “solución local”, puede verse como parte de la tienda; sin embargo, aún representa una tarea de integración que debe implementarse de manera correcta, robusta y con alto rendimiento. Con un pequeño ejemplo de código de una tarea de integración de este tipo, me gustaría analizar las diferencias entre las buenas y las malas prácticas.

El buen código existe más allá del zumbido

Desde el punto de vista de casi 40 años de práctica en desarrollo de software, ciertas buenas y malas prácticas son temas recurrentes en el día a día de un desarrollador de software. En lugar de eliminar las malas prácticas, a menudo se ponen sobre la mesa nuevas exageraciones y palabras de moda. Se espera que proporcionen una «solución casi automática» a los problemas de software. El software a menudo es un trabajo por encargo, que se supone que mejora el llamado «resultado final» de una empresa comercial y proporciona a los clientes soluciones sólidas y operables. Por lo tanto, me gustaría suplicar que no se dedique demasiado tiempo a buscar el Santo Grial en la forma de la exageración actual, sino que se desarrollen las propias habilidades en escritura software robusto.

Un típico anti-patrón…

Me gustaría ilustrar esto con un ejemplo bastante corto derivado de un proyecto real, que muestra un anti-patrón persistentemente perseguido:
El software para crear un índice de búsqueda muestra un comportamiento extraño en términos de manejo de varios idiomas. El texto alemán termina en el índice del idioma inglés, aunque la lógica de indexación aparentemente establece el idioma correctamente. Una parte esencial del código es la clase Indexer, que se ve así en forma reducida en pseudocódigo:

class Indexer
{
  private int langId = 0;
  private string langIsoCode = zero;

indexador público ()
{
}

setLanguageId público (identificación int)
{
this.langId = id;
}

público int getLanguageId()
{
devolver este.langId;
}

public setLanguageIsoCode(String isoCode)
{
this.langIsoCode = isoCode;
}

cadena pública getLanguageIsoCode()
{
devolver este.langIsoCode;
}

índice público (clave de cadena)
{
Vista de cadena = Database.getLanguageViewFor(this.langId);
Valor de cadena = Database.readFromView(vista, clave);
this.sendToIndex(clave, valor);
}

sendToIndex privado (clave, valor)
{
// no incluido aquí
}
}

Esta bonita clase hace algunas cosas más (¿quizás no una clara «Separación de preocupaciones»?), pero se usa de la siguiente manera:

String keyToIndex = 'myKey';
Indexer indexer = new Indexer();

foreach (Idiomas: String langIsoCode) {
indexer.setLanguageIsoCode(langIsoCode);
indexador.índice(claveAlÍndice);
}

Esto parece razonable a primera vista, especialmente cuando está incrustado en una base de código más grande.
Sin embargo, el problema es que Indexer tiene dos variables miembro langId y langIsoCode, que deben estar restringidas (el ID siempre debe coincidir con el código ISO). Sin embargo, esta restricción no existe en el código, sino en el mejor de los casos en la mente del desarrollador, y no se impone mediante la construcción de esta clase.

… y los problemas que de ello se derivan

Los problemas ya comienzan en la fase de construcción, lo que permite la creación de un objeto disfuncional. Los setters y getters públicos permiten cualquier cambio externo que pueda violar la conexión entre el ID y el código ISO. Estos problemas persisten a lo largo del ciclo de vida del objeto: el usuario debe ser meticulosamente cuidadoso para establecer siempre ambos argumentos para que coincidan de antemano. Así que esta clase también tiene un gran problema de usabilidad.

Sería mejor crear Indexer como una clase no mutable, que toma solo el ID o el código ISO del lenguaje como parámetros de construcción y, por lo tanto, solo se puede configurar correctamente y ya no se puede poner en un estado incorrecto llamando a un método de mutación.

Si la clase realmente tiene que ser mutable (lo que a menudo se puede evitar), entonces no debería haber ninguna mutación que cree un estado inválido. Imagine un setter para ID y código ISO o un setter que toma ambos como parámetros y puede reconocer una combinación incorrecta de los dos parámetros.

Prueba cruzada: ¿El patrón puntúa con flexibilidad?

Pero, ¿con qué frecuencia un diseño como el anterior ha sido defendido por un programador como deseable porque es «flexible»? Después de todo, en muchos libros (que quizás nunca deberían haberse impreso) se muestra exactamente ese estilo. Como ya no puedo contar la cantidad de errores que se pueden rastrear hasta este antipatrón, lo llamo «desafiar al destino».

La amada «flexibilidad» con demasiada frecuencia conduce a que una instancia de dicha clase se coloque en un estado no válido y la conexión entre causa y efecto a menudo no es fácil de reconocer, ya que ambos están muy separados en la base del código. En una inspección más cercana, el antipatrón descrito no es una flexibilización del código, por ejemplo, por interfaces y polimorfismo, sino la introducción de no determinismos.

La inversión de control o principio de Hollywood (“no nos llames, nosotros te llamamos”), por otro lado, contribuye mucho más a la robustez del software. En el mundo OO, esto se puede encontrar en marcos o en forma de patrón de «método de plantilla», en el mundo funcional se puede encontrar en Erlang/OTP como los llamados Comportamientos. La razón por la que esta técnica de diseño conduce a un código robusto es simple: aquí, la reutilización se realiza de la mejor manera SECA (no se repita) y los errores en un marco (o Comportamiento) se reducen con cada realización concreta del marco.

Un efecto secundario agradable es que el comportamiento del software basado en tales principios es más predecible y no muestra un comportamiento inesperado. De esta manera, el desarrollador ya no desafía al destino, sino que se pone a sí mismo en control.

Flexibilidad frente a robustez

Lo interesante es que las técnicas de programación robusta han existido durante mucho tiempo y todavía se aplican con éxito en los ecosistemas de software modernos. Sin embargo, las malas prácticas siguen estando muy extendidas y, por tanto, son difíciles de erradicar. Parece que tienes que experimentar por ti mismo que tu vida como desarrollador de software no se hace más fácil con diseños “flexibles”, sino más bien con diseños robustos. La razón de esto es que el esfuerzo principal no es la creación rápida de una «prueba de concepto», sino la llamada fase de mantenimiento, en la que se deben corregir los errores e implementar nuevos requisitos de tal manera que no introduzcan cualquier error nuevo.

Este artículo se publicó por primera vez en alemán como «Gute und schlechte Softwarepraktiken gestern und heute» en www.oxid-esales.com/blog.

Sobre el Autor:

.

Dr. Bernhard Scheffold ha sido arquitecto de software en OXID eSales AG desde 2012. Anteriormente, el físico desarrolló software industrial para bancos, compañías de seguros, el sector de la salud, así como para la industria y la investigación genética.
Para OXID Professional Services, el Dr. Scheffold supervisa proyectos para medianas empresas y corporaciones con un enfoque en el análisis y la integración de sistemas, así como en el ajuste del rendimiento.
También enseña los principios de la creación de software y arquitectura robustos y evolutivos como instructor del curso «Ingeniería de línea de productos con marcos» en Digicomp AG, Suiza.

Deja una respuesta

Tu dirección de correo electrónico no será publicada.