Programación pragmática
Del libro The Pragmatic Programmer
Filosofía pragmática
-
Kaizen -> pequeños progresos continuos
-
Responsabilidad:
- admitir errores y desconocimiento
- preguntar y pedir ayuda
- soluciones, no excusas
- no echar la culpa a otros ( → si no es achacable a nosotros, deberíamos haberlo previsto y tener un plan de contingencia)
-
Ventanas rotas: (pequeños errores)
- no permitirlas
- arreglar cualquier pequeño error, mal código o mal diseño (aunque parezca que no tiene importancia), en cuanto se descubra
-
La sopa de piedras (y la de ranas)
- es mejor pedir perdón que permiso
- es más sencillo unirse a algo que ya está en marcha que empezar desde cero
- estar pendiente de todo lo que pase alrededor, no sólo de lo que hace uno mismo; algo insignificante se puede hacer muy importante sin que nos demos cuenta
-
No tocar lo que ya esté suficientemente bien
- intentando conseguir la perfección, podemos estropear algo que ya funciona
- si es suficientemente bueno para mí, para los usuarios y para quien tenga que mantenerlo en un futuro → se puede dejar así, no es necesario intentar mejorarlo
- implicar a los usuarios en las soluciones de compromiso que se toman
- además de pedirles los requisitos, hay que preguntarles cómo quieren que sea de buena la solución
- el alcance y la calidad se deberían incluir siempre con la toma de requisitos (no sólo basta con saber qué se quiere)
- los usuarios por lo general prefieren tener ya una versión incompleta, que esperar mucho tiempo por tener algo
- es preferible una buena aplicación hoy que una perfecta mañana
-
hay que saber cuándo parar
- programar es como pintar un cuadro; si no se para a tiempo y se siguen añadiendo detalles se puede arruinar el resultado
Aproximación pragmática
- Las maldades de la duplicidad
- Hay que intentar no duplicar el conocimiento en los requisitos, procesos y código.
- DRY: don’t repeat yourself
- Cada pieza de conocimiento debe tener una única, no ambigua y autorizada representación dentro de un sistema.
- Tipos de duplicidad (y técnicas para evitarla):
- impuesta
- múltiples representaciones de la información
- se pueden hacer con un generador de código
- documentación en el código. La documentación debe ser a un alto nivel para que cada cambio en el código no implique cambiar la documentación
- primero hay que documentar y luego pasar a codificar (si se hace al revés al final no se documenta)
- duplicidades obligatorias por el lenguaje de programación (por ejemplo sí obliga a separar declaración e implementación)
- es difícil evitarlo, pero al menos ¡no hay que duplicar los comentarios!
- múltiples representaciones de la información
- inadvertida
- viene de errores en el diseño
- impaciente
- evitar atajos provocados por las prisas
- entre-desarrolladores
- por ejemplo, cada programa con su propia validación del dni
- la mejor manera de evitarlo es la comunicación entre desarrolladores
- impuesta
- Ortogonalidad
- Dos cosas son ortogonales si cambiar una no afecta a la otra. Por ejemplo, deberían ser ortogonales base de datos e interfaz de usuario
- Mayor ortogonalidad en el sistema da más calidad.
- hay que eliminar efectos entre cosas no relacionadas.
- cohesión
- los sistemas ortogonales aumentan la productividad y reducen el riesgo
- ortogonalidad en los equipos de proyecto
- se consigue separando el equipo en grupos por infraestructuras (uno para bases de datos, otro grupo para servidor de aplicaciones, etc.) y por funcionalidades (uno para FC otro para fpo, etc.)
- una vez creados los grupos ajustamos las personas de las que disponemos a estos grupos.
- se consigue separando el equipo en grupos por infraestructuras (uno para bases de datos, otro grupo para servidor de aplicaciones, etc.) y por funcionalidades (uno para FC otro para fpo, etc.)
- Mantener ortogonalidad en el código
- Mantener el código separado
- Evitar datos globales
- tener cuidado con los patrones singleton en java, que se suelen utilizar para tener datos globales
- Evitar funciones similares
- Tenemos que ser críticos con nuestro código y tener el hábito de mejorarlo y reorganizarlo para hacerlo más ortogonal (refactorización)
- Las pruebas son más sencillas en sistemas ortogonales, ya que tenemos tests unitarios en vez de tests de integración
- cada módulo debería tener su propio test unitario
- la documentación también debe ser ortogonal, teniendo separadas la forma y el contenido
- Reversibilidad
- Las decisiones críticas no son fácilmente reversibles
- Es importante evitarlas lo máximo posible
- No existen las decisiones definitivas
- Hay que mantener flexibles:
- código
- arquitectura
- despliegue
- integración con proveedores
- Las decisiones críticas no son fácilmente reversibles
- Tracer Bullets (balas trazadoras ?)
- Es preferible utilizar balas trazadoras a definir todos los requisitos completamente (ya que esto llevaría demasiado tiempo y siempre se cambian)
- La diferencia entre código "trazador" y código normal es que el trazador no es completo y se le va añadiendo funcionalidad poco a poco. Pero no tiene que tener menor calidad, documentación, ni pruebas, …
- Aproximación incremental
- Ventajas:
- Los usuarios tienen más rápidamente algo que ver y con lo que trabajar
- Los desarrolladores construyen una estructura sobre la que trabajar
- Se tiene una plataforma de integración.
- Es mejor hacer pequeñas integraciones cada día, que hacer una gran integración cada cierto tiempo
- Se tiene algo que enseñar
- Se tiene mayor sensación de progreso
- Código traceador frente a prototipado
- La diferencia es que con un prototipo se exploran aspectos específicos del producto final; una vez utilizado se desecha y se recodifica completamente para obtener el producto definitivo
- Prototipos y pos-it
- Los pos-it son muy buenos para el prototipo del workflow y de la lógica de la aplicación.
- Cualquier cosa que implique riesgos es un posible candidato a prototiparlo
- Hacer prototipos es una experiencia de aprendizaje
- Lenguajes del dominio
- En lugar de empezar a pensar una solución en un lenguaje de programación, es buena idea empezar con el lenguaje del dominio del problema.
- Inicialmente, puede utilizarse para recoger los requisitos
- Se puede llegar a implementar este lenguaje.
- En lugar de empezar a pensar una solución en un lenguaje de programación, es buena idea empezar con el lenguaje del dominio del problema.
- Estimación
- Hay que estimar para evitar sorpresas
- La mejor estimación sobre el tiempo que lleva hacer algo se obtiene de alguien que ya lo haya hecho
- Es importante guardar las estimaciones y compararlas con el resultado real
- Es conveniente recalcular la estimación en cada iteración del proyectos
Herramientas básicas
- Texto plano
- Es la mejor forma de guardar información
- Filosofía de Unix
- Línea de comandos
- Es más sencillo y potente automatizar tareas desde la línea de comandos que en modo gráfico
- Hay tareas (como búsquedas complejas) que son prácticamente imposibles de llevar a cabo en modo gráfico
- En windows se puede usar Cygwin
- Power Edition
- Lo ideal es utilizar siempre el mismo editor (tanto para escribir código, como documentación, como comandos de shell…)
- Control de versiones
- Utilizar siempre control de versiones, tanto para código como para documentación
- Depuración
- Se trata de corregir el problema, no de encontrar el culpable
- Es importante corregir todos los errores (y avisos) de compilación, para que la depuración sea más sencilla.
- Rubber ducking (hablar al muñequito 😉
- Manipulación de texto
- dominar uno de los siguientes lenguajes/herramientas de manipulación:
- awk, sed
- tcl
- python
- perl → el recomendado
- dominar uno de los siguientes lenguajes/herramientas de manipulación:
- Generadores de código
- Escribir código que escriba código
Paranoia pragmática
- Es imposible escribir software perfecto
- Diseño por contrato (DBC – Design By Contract)
- Un contrato define nuestros derechos y responsabilidades, así como los de la otra parte
- Para cada módulo o función de la aplicación debe definirse:
- precondiciones
- poscondiciones
- clases invariantes
- Los programas muertos no mienten
- Programación asertiva
- Cada vez que pensemos que algo no va a suceder nunca, agregar al código una aserción para comprobarlo
- en C ¡y en Java! se hace con assert
- Normalmente se hace que las aserciones sólo se tengan en cuenta en tiempo de compilación, quedando en la ejecución deshabilitadas. Pero se deberían habilitar siempre.
- Cada vez que pensemos que algo no va a suceder nunca, agregar al código una aserción para comprobarlo
- Cuándo usar excepciones
- No se deben utilizar para el flujo normal de programa, deberían usarse sólo para sucesos inesperados. – Teniendo en cuenta, que una excepción no capturada terminará el programa, nos podemos preguntar si el código seguirá ejecutándose si quitamos todos los manejadores de excepción. En este caso, posiblemente las excepciones se estén utilizando en un circunstancias no excepcionales, y no habría que utilizarlas.
- Cómo balancear recursos
- Termina todo lo que empieces (cerrar ficheros abiertos, …)
- Asignaciones anidadas:
- Desasignar recursos en el orden inverso en que se han asignado
- Cuando se asigne el mismo conjunto de recursos en dos partes distintas del código, hacerlo en el mismo orden (para reducir la posibilidad de bloqueos).
- Quien sea que asigne un recurso debe ser el encargado de desasignarlo
- Objetos y excepciones
- En POO es útil encapsular los recursos en clases. Cuando se necesita el recurso se instancia un objeto de dicha clase; en el momento en que el objeto esté fuera de alcance, o hay sido reclamado por el garbage collector, el destructor del objeto liberará el recurso.
- Balanceo y excepciones
- Si se arroja una excepción, ¿cómo podemos asegurar que todos los recursos se liberarán correctamente?
- En Java se pueden liberar los recursos en la cláusula finally
- Si se arroja una excepción, ¿cómo podemos asegurar que todos los recursos se liberarán correctamente?
Doblarse o romperse
- Desacoplamiento y la Ley de Demeter
- Minimizar el acoplamiento
- Reducir la interacción entre módulos
- El problema no está en que un módulo interactúe con otro, sino en cómo lo haga.
- Los sistemas con muchas dependencias innecesarios son difíciles y caros de mantener y tienden a ser más inestables.
- La ley de Demeter para funciones, o ley del menor conocimiento, intenta minimizar el acoplamiento entre módulos en un programa.
Dice que cualquier objeto de un método sólo debería invocar objetos que pertenecieran a:- sí mismo,
- parámetros que se hayan pasado al método
- objetos que hayan sido creados en el método
- cualquier componente del objeto
- Hay que tener en cuenta que minimizar el acoplamiento, utilizando por ejemplo la ley de Demeter, irá en detrimento del rendimiento de la aplicación. Por lo que habrá que encontrar un equilibrio.
- Minimizar el acoplamiento
- Metaprogramación
- Programar para el caso general, y poner los datos particulares en otro sitio, fuera del código base.
- Acoplamiento temporal
- Normalmente, al desarrollar software no se piensa en el tiempo (salvo en el tiempo de entrega). El tiempo tiene dos aspectos importantes que se deberían tener en cuenta::
- Concurrencia: cosas que pasan a la vez
- Orden: posiciones de las cosas en el tiempo
Sin embargo, al programar se suele pensar sólo de una forma lineal, sin tener en mente ni la concurrencia ni el orden de las cosas. Esta forma de pensar es la que lleva al acoplamiento temporal.
- Normalmente, al desarrollar software no se piensa en el tiempo (salvo en el tiempo de entrega). El tiempo tiene dos aspectos importantes que se deberían tener en cuenta::
- Flujo de trabajo. Se puede modelar el flujo de trabajo mediante el diagrama de actividades de uml.
Analizando el flujo de trabajo se mejorará la concurrencia. - Arquitectura. Diseñar utilizando servicios, en lugar de componentes.
- Diseñar para la concurrencia
- Hacer interfaces más limpias
- Sólo es una vista
- Separar el modelo de la vista
- MVC (Modelo-Vista-Controlador)
- Pizarras
- JavaSpaces, T Spaces
Mientras programas
- La sabiduría popular dice que la fase de programación es mecánica. Esta actitud es la única razón de que la mayoría de los programas sean feos, ineficientes y difíciles de mantener.
- Programar no es algo mecánico; los desarrolladores deben pensar mucho durante su trabajo y no dedicarse a hacer tareas puramente mecánicas.
- Programando por coincidencia
- Si no sabemos porqué funciona el código que hacemos, cuando falle no sabremos porqué falla ni cómo solucionarlo
- Velocidad algorítimica
- Refactorizar
- El código no es estático, necesita evolucionar.
- El desarrollo de software se parece más a la jardinería que a la construcción de un edificio.
- ¿Cuándo refactorizar?
- Duplicación ← contra el principio DRY
- Diseño no ortogonal
- Conocimiento desfasado
- Rendimiento
- Refactoriza pronto, refactoriza a menudo
- Cómo refactorizar:
- No trates de refactorizar y añadir una nueva funcionalidad a la vez
- Asegurarse de que se tienen buenos tests antes de refactorizar
- Hacer pequeños pasos, y hacer los tests después de cada paso
- Código fácil de probar
- Hay que pensar en las pruebas desde el inicio de la construcción del software; y probar cada pieza antes de probar el conjunto.
- Pruebas unitarias
- Un test unitario establece un entorno artificial y entonces invoca al módulo que queremos probar.
- En las pruebas del producto total también se invocará los tests unitarios para volver a probar las partes.
- Probar el contrato
- Si se ha programado según un contrato, lo podemos utilizar para crear los tests unitarios.
- Cada vez que se diseña un módulo deberíamos diseñar tanto el contrato como el código que pruebe dicho contrato
- Escribiendo tests unitarios
- En pequeños proyectos se puede incluir el test dentro del propio módulo a probar. Para proyectos grandes, es mejor tener un subdirectorio para cada test.
- A los desarrolladores para que ejecuten los tests hay que darles:
- ejemplos de cómo usar la funcionalidad del módulo
- formas de construir tests de regresión para validar cualquier futuro cambio en el código
- Es conveniente que cada clase o módulo tenga su propio test unitario. Por ejemplo, en java cada clase puede tener su main, que se puede utilizar para ejecutar los tests (este main se ignora cuando se ejecuta la aplicación -ya que sólo se tiene en cuenta el main del módulo principal-).
- Esqueletos para los tests unitarios
- Se puede crear una clase base que implemente las operaciones comunes como log, analizar salidas, etc.
- En java se puede utilizar reflexión para para construir dinámicamente una lista de tests (principio DRY)
- Se puede utilizar xUnit (jUnit en Java) para hacerlo
- Este esqueleto debe incluir:
- Una forma estándar para especificar la configuración y el limpiado
- Un método para elegir tests individualmente o bien todos los tests
- Formas de analizar salidas esperadas o inesperadas
- Informes estandarizados
- Crear una ventana para los tests
- A veces los errores sólo ocurren en producción. Tenemos que ser capaces de ejecutar los tests en producción para poder extraer conclusiones (una puerta de atrás)
- Asistentes malvados (evil wizards)
- No utilizar asistentes que no entendamos, o sólo seremos capaces de programar por coincidencia
Antes del proyecto
- El pozo de los requisitos
- Documentar las razones detrás de los requisitos
- No es suficiente con saber qué quiere hacer el usuario, saber porqué nos va a ayudar a entender mejor lo que tenemos que hacer
- Trabajar con un usuario para pensar como un usuario
- Documentando requisitos
-
Casos de uso
-
Plantilla de caso de uso
- Información característica
- Objetivo en contexto
- Escenario
- Nivel
- Precondiciones
- Condición final de éxito
- Condición final de fallo
- Actor principal
- Disparador
- Escenario principal de éxito
- Extensiones
- Variaciones
- Información relacionada
- Prioridad
- Objetivo de rendimiento
- Frecuencia
- Caso de uso padre
- Casos de uso hijos
- Canal al actor principal
- Actores secundarios
- Canal a los actores secundarios
- Agenda
- Problemas abiertos
-
-
Diagrama de casos de
uso
- No se puede documentar sólo con los diagramas
-
- Sobreespecificación
- Es un peligro en la toma de requisitos
- Viendo más allá
- Las abstracciones viven más que los detalles
- Mantener un glosario
- Tiene que ser accesible por todos los participantes del proyecto (desarrolladores, usuarios, soporte, …). Es buena idea tenerlo en formato web.
- Correr la voz
- Hacer accesible la documentación de los requisitos (por ejemplo, en una web).
- Documentar las razones detrás de los requisitos
- Resolviendo puzzles imposibles
- nudo gordiano
- ¿existe una forma más sencilla?
- No hasta que estés preparado
- La trampa de la especificación
- Algunas cosas es más fácil hacerlas que explicarlas
- Un diseño que no deja ningún lugar a la interpretación al programador, le quita todo el esfuerzo de cualquier habilidad o arte.
- Esto no debe hacerse. Hay posibilidades que sólo surgen durante la codificación.
- No debe especificarse absolutamente todo en el diseño.
- Círculos y flechas
- No seas esclavo de los métodos formales
Proyectos pragmáticos
- Equipos pragmáticos
- Automatización omnipresente
- No usar procedimientos manuales
- Automatizar la compilación del proyecto
- Pruebas despiadadas
- Todo hay que escribirlo
- La documentación es un parte integral del proceso de desarrollo
- Tipos de documentación
- La programación interna incluye comentarios en código, diseño y documentos de prueba
- La documentación externa incluye manuales de usuario
- Toda la documentación debe ser un espejo del código
- Comentarios en el código
- El código debe tener comentarios, pero demasiados comentarios son tan malos como pocos.
- El comentario debe contar porqué se hace algo, su propósito y objetivo. Cómo se hace ya lo explica el propio código
- Lo que nunca debe comentarse en el código:
- lista de funciones
- histórico de versiones ← para esto está el control de versiones
- puede ser útil incluir información sobre el último cambio (quién y cuándo lo hizo)
- lista de ficheros que utiliza éste
- nombre del fichero
- Lo que debe comentarse:
- autor
- comentarios tipo javadoc
- Documentar ejecutables
- Si la documentación no está en texto plano (que sería lo deseable) se puede hacer lo siguiente para extraer información (para, por ejemplo, a partir de la documentación generar los sqls de creación de tablas):
- Escribir macros
- Hacer el documento ejecutable
- Se puede generar documentación a partir del código mediante herramientas como JavaDoc. A partir del código (maestro) se obtiene la documentación (vistas)
- Si la documentación no está en texto plano (que sería lo deseable) se puede hacer lo siguiente para extraer información (para, por ejemplo, a partir de la documentación generar los sqls de creación de tablas):
- Escritores técnicos
- Lenguajes de marcado
- DocBook
- Grandes Esperanzas
- El éxito de un proyecto se basa en cómo satisface las expectativas de sus usuarios.
- El kilómetro extra
- Hay que intentar sorprender a los usuarios y darles algo más de lo que esperan
- Orgullo y Prejuicio
- Firma tu trabajo