BUENAS PRÁCTICAS EN J2EE. PRIMERA PARTE. VERSION PARA IMPRIMIR (Versión original en español en http://espanol.aurumsol.com/articulos/art6/art6-1.html Enlace a la versión original en inglés) Informe de The Middleware Company Traducido y abreviado con permiso por Dr. Vicent-Ramon Palasí Lallana. Gerente General de Aurum Solutions. http://www.aurumsol.com Noviembre 2003 J2EE (“Java 2 Enterprise Edition” o Edición empresarial de Java 2) ha emergido, en los últimos años, como una plataforma estándar para desarrollar aplicaciones empresariales. Aunque está madurando, la plataforma J2EE presenta todavía una serie de desafíos como, por ejemplo, rendimiento, gestión de recursos y flexibilidad. Los programadores que usan la tecnología J2EE deben aprender a ir más allá de la sintaxis de Java y comprender cuál es la mejor forma de diseñar e implementar nuevas soluciones con J2EE. Para ello, si no se tiene una amplia experiencia J2EE en el mundo real, la mejor opción es aprender de la experiencia de otros. La plataforma J2EE demuestra cada vez más sus fortalezas y debilidades en entornos de producción. Usted puede utilizar eficazmente esta experiencia si comprende las buenas prácticas que se incluyen en este artículo. ¿Qué es exactamente una buena práctica? Una buena práctica es una técnica comprobada para conseguir un resultado deseado. Es una técnica repetible que ha sido exitosa en situaciones de la vida real y que puede aplicarse ampliamente a muchos problemas. Estudiaremos las buenas prácticas asociadas a cinco etapas generales que cualquier metodología de desarrollo debe incluir: diseño, programación, pruebas, despliegue y afinación (se omite la fase de análisis por no ser específica de la plataforma J2EE). También explicaremos las buenas prácticas aplicables al ciclo y a los entornos de desarrollo. Por su longitud, dividimos este informe en dos partes, la primera de las cuales se incluye a continuación. 1. BUENAS PRÁCTICAS SOBRE EL CICLO DE DESARROLLO 1.1. Buena práctica número 1. Enfrente el riesgo lo más pronto posible El riesgo asociado con el desarrollo crece exponencialmente con la complejidad de la aplicación. Aunque este problema parece no tener solución, hay algunas técnicas que pueden aplicarse para mitigar los riesgos. En lo que respecta al proceso de desarrollo, la mitigación de riesgos más importante consiste en desplazar el riesgo al principio del cronograma. De esta forma, se reduce el riesgo por tres motivos: Se mejora el conocimiento. Los mayores riesgos se asocian siempre con lo desconocido. Un conocimiento más temprano permite hacer decisiones más informadas y definir mejores cronogramas a lo largo del ciclo de desarrollo. Se permite tiempo de recuperación. Si se fracasa, es mejor fracasar pronto, cuando hay todavía tiempo para recuperarse. Se involucra tempranamente al cliente. La respuesta del cliente a una interfaz de usuario o a una función clave puede impactar de forma dramática la dirección de un proyecto. Además, involucrar tempranamente al cliente mejora las relaciones con el mismo, al inspirarle confianza de forma temprana. Pruebas de concepto en forma de T Las mayores áreas de riesgo de un proyecto suelen incluir la interfaz de usuario y los componentes clave de la capa de negocio. Se suelen usar enfoques diferentes para tratar cada una de estas áreas. Para la primera de ellas, se utiliza el prototipado de interfaces de usuario, que consiste en programar rápidamente unas cuantas interfaces aproximadas, para obtener retroalimentación del usuario de forma temprana. Para la segunda área, se suele usar una “prueba de concepto” tecnológica que consiste en implementar un único requerimiento del sistema, pero de manera completa y profunda, con código de producción. Como se ve en la figura 1, el prototipo implementa una capa muy amplia, pero muy poco profunda. En cambio, la prueba de concepto es muy profunda pero muy estrecha. Figura 1. El enfoque en forma de T mitiga tempranamente el riesgo El método más efectivo para mitigar el riesgo usa un enfoque híbrido, combinando ambos métodos en una prueba de concepto en forma de T, como se muestra en la figura 1. Se construye un prototipo de interfaz de usuario muy poco profundo y se integra con una prueba de concepto de un único requerimiento con una implementación completa de todos sus aspectos. La interfaz del prototipo será muy poco profunda, teniendo solamente en cuenta el contenido y el flujo entre pantallas pero no la lógica de negocio ni la apariencia estética. Por el contrario, el requerimiento de la prueba de concepto se programará de manera completa, profunda y robusta, con tanto código de producción como sea posible y con el mínimo andamiaje necesario. Cuando se acabe la T y funcionen el modelo de interfaz de usuario y el modelo técnico funcionen, el proceso continúa expandiéndose desde la barra central de la T para implementar más requerimientos. Este enfoque “en forma de T” es una herramienta efectiva para mitigar riesgo, desplazándo dos áreas de incertidumbre hacia adelante Otras técnicas para mitigar el riesgo La prueba de concepto en forma de T no es la única forma para mitigar riesgo. Las siguientes técnicas son también efectivas: Reserve tiempo suficiente para hacer investigación y aprendizaje de tecnologías críticas en forma temprana. Así aumentará su conocimiento sobre los límites de estas tecnologías. Antes del fin de un ciclo de proyecto, separe un equipo de programadores para que comprendan los requerimientos y tecnologías del siguiente ciclo. Utilice tutores. Si no tiene ninguno en la empresa, use a sus proveedores de tecnología o encuentre otros consultores. 2. BUENAS PRÁCTICAS DE DISEÑO. Las metodologías modernas de desarrollo ponen un gran énfasis sobre la etapa de diseño pues contribuye enormemente a la calidad, escalabilidad y fiabilidad de una aplicación. Si diseñamos los componentes de una aplicación de forma reusable y escalable, el código que programemos en el futuro se construirá sobre unos cimientos sólidos. Se necesita tratar varios aspectos en esta etapa, como, por ejemplo, la seguridad, el modelo subyacente de persistencia y los recursos de la aplicación. Para la mayoría de las aplicaciones, los problemas comunes tienen soluciones bien conocidas. Trataremos algunas de ellas en el presente apartado. 2.1. Buena práctica número 2. Diseñe para el cambio con un modelo de dominio dinámico. Con la aparición del desarrollo iterativo y de ciclos de desarrollo más breves, es casi seguro que una aplicación sufrirá cambios después del diseño inicial. Para manejar efectivamente estos cambios y conseguir un alto nivel de calidad, los programadores no deberían volver a programar código ya probado desde cero. Para evitar esto, se necesitan modelos de dominio dinámicos, es decir, que cambien de forma ágil. Existen varias soluciones posibles para implementar un modelo de dominio dinámico. Las más comunes son el uso de “beans” de entidad CMP en EJB o de un motor de persistencia convencional como Toplink. Estas herramientas liberan a los programadores de la tarea de mantener el acceso a datos y les permiten concentrarse en las reglas de negocio. Como consecuencia, cuando se cambie el esquema de la base de datos, se deberán cambiar muchas menos líneas de código que las que sería necesario modificar si no se utilizaran esas herramientas. 2.2 Buena práctica número 3. Utilice un lenguaje de modelado estándar. Los diseñadores y programadores, así como cualquier persona involucrada en el proceso de desarrollo, necesitan un lenguaje común para poder comunicarse claramente. El lenguaje unificado de modelado (“Unified Modelling Language” o UML) provee un lenguaje y una variedad de diagramas para que los arquitectos y diseñadores puedan explicar conceptos técnicos complejos de forma sencilla y precisa. Muchos entornos de desarrollo en Java ofrecen herramientas para generar código a partir de modelos UML y a la inversa. Esas herramientas permiten un código bien documentado y una mayor productividad del programador, sin cargar a este último con el peso de sincronizar el código con los diagramas UML. 2.3 Buena práctica número 4. Recicle sus recursos. Fondo Común de Objetos Las aplicaciones suelen desperdiciar demasiado tiempo recuperando, creando o destruyendo algunos objetos o recursos complejos. Para evitar esto, se debería crear un número limitado de recursos y compartirlos desde un fondo común (“pool”, en inglés). J2EE gestiona algunos de estos fondos, como, por ejemplo, los de conexiones. Accediendo a un recurso existente mediante un fondo común, el cliente disminuye los costos de creación, destrucción e inicialización. Cuando el cliente acaba con el objeto, devuelve el objeto al fondo común para que sea utilizado por otros clientes. Caché Desde hace mucho tiempo, la industria de la computación ha utilizado cachés de hardware y software para mejorar el rendimiento desde el nivel más bajo de hardware a la capa de software más alta. Una caché no es más que un tipo específico de fondo común. En vez de guardar en el fondo una conexión u objeto, se guardan datos remotos, desplazándolos a una ubicación más cercana al cliente. Muchos vendedores de hardware y software suministran sus propias cachés de datos para sus servidores Web y servidores de bases de datos. A pesar de ello, puede ser útil programar cachés específicas para tareas especiales, como servicios Web, solicitudes cruzadas de red u otras. 3. BUENAS PRÁCTICAS DE PROGRAMACIÓN. Por décadas, la etapa de programación fue una fase larga y realizada de manera contigua. Sin embargo, la mayoría de metodologías modernas rompen esta etapa en iteraciones más pequeñas, para reducir su riesgo y complejidad. Como consecuencia, los programadores necesitan escribir código altamente flexible y reusable y hacerlo muy rápidamente, lo que se logra aplicando las técnicas que se explican en este apartado. 3.1. Buena práctica número 5. Utilice patrones de diseño probados Los patrones de diseño están entre los recursos más importantes para arquitectos y programadores ya que pueden usarse como se proponen o aplicarse a nuevos contextos. Los patrones están probados y son reusables. Proveen un lenguaje común para expresar la experiencia en programación. En pocas palabras, los patrones de diseño ahorran tiempo, dinero y esfuerzo. Por brevedad, sólo presentamos algunos de los patrones J2EE más importantes. Para una referencia más completa, consulte el catálogo de patrones J2EE de Sun. “Session Façade” (Fachada de sesión). Las aplicaciones EJB de grado fino son muy propensas a presentar una sobrecarga en las comunicaciones. Las fachadas de sesión proveen una capa de acceso a servicios que oculta la complejidad de las interacciones subyacentes y combina muchas comunicaciones lógicas en una única comunicación física, mejorando el rendimiento. MVC. El patrón de diseño Modelo-Vista-Controlador desacopla las capas de acceso a datos, presentación de los datos e interacción con el usuario. Los beneficios que resultan son un mantenimiento más sencillo porque hay menos dependencias entre componentes, una mayor reusabilidad pues los diferentes componentes implementan funciones distintas y una configuración más sencilla, ya que se pueden implementar nuevas vistas sin cambiar ningún código subyacente. “Value Objects” (Objetos de valor). Los objetos de valor son una única representación de todos los datos que se necesitan en una llamada a un EJB. Combinando todos los datos requeridos por una serie de llamadas, se puede ejecutar una única llamada remota en vez de muchas, aumentando el rendimiento global de la aplicación. “Data Access Objects” (Objetos de acceso a datos). Utilice un objeto de acceso a datos para abstraer y encapsular todo el código de acceso al origen de datos (normalmente, una base de datos). El objeto de acceso a datos gestiona la conexión con dicho origen de datos para recuperar y guardar datos, impidiendo que la lógica de acceso a datos se mezcle con la lógica del negocio. “Service Locator” (Localizador de servicios). Las aplicaciones empresariales necesitan el uso de componentes distribuidos. A menudo, es una tarea difícil y costosa obtener referencias a los componentes, como por ejemplo a una interfaz “home” EJB. En su lugar, se pueden gestionar dichas referencias a partir de un localizador de servicios que centralice las búsquedas de objetos distribuidos. Algunos beneficios de ello es contar con un único punto de control y con un punto adecuado para colocar una caché. 3.2. Buena práctica número 6. Automatice el proceso de construcción En un entorno de desarrollo empresarial, el proceso de construcción de una aplicación (“build process”) puede resultar muy confuso. Las interrelaciones entre archivos, los programadores que se centran en un único aspecto y los componentes de software ya empaquetados son factores que se añaden a la complejidad de dicho proceso. Si el proceso se ejecuta de forma manual, se pueden pasar incontables horas intentando descubrir como construir todos los componentes en el orden correcto. Por ello, es mucho mejor automatizar este proceso, para lo cual se deben considerar tres aspectos importantes: Se deben escoger herramientas adecuadas para dicha automatización. El proceso de desarrollo y el proceso de construcción deben interoperar bien. El proceso de construcción debería notificar el éxito o fracaso y permitir configurar los criterios que definen este éxito o fracaso. Si usted trabaja con la línea de comandos, debería usar una herramienta de construcción. Hoy en día, Ant es la herramienta escogida por los programadores de Java en todo el mundo. Ant es una herramienta de construcción extensible que está escrita en Java. Es un proyecto de código abierto de Apache Jakarta que se ejecuta sobre múltiples plataformas y puede contribuir en gran medida a la flexibilidad del proceso de construcción. 3.3. Buena práctica número 7. Integre con frecuencia El proceso de construcción es una parte integral del desarrollo de aplicaciones. Es la primera oportunidad para tratar los errores de integración. Por su importancia, los procesos de construcción deberían tener lugar tan frecuentemente como sea posible, una vez se haya profundizado lo suficiente en el proceso de desarrollo para tener código de integración. La integración continua puede requerir una construcción diaria en las aplicaciones de mayor tamaño o incluso construcciones que tengan lugar cada vez que se registra un código en la herramienta de control de versiones. El motivo de todo ello es evidente. La integración empeora con el tiempo. Cuanto más tiempo exista entre integraciones, mayor será el esfuerzo necesario para la integración. Mediante una integración constante, se pueden identificar errores y depurarlos el mismo día en que se añaden al sistema. Además, esta buena práctica fomenta entre los programadores una cultura de respeto mutuo y responsabilidad. 3.4. Buena práctica número 8. Optimice los costos de comunicación La comunicación distribuida puede ser bastante eficiente. Sin embargo, cuando tiene lugar dentro de bucles y sobre una costosa infraestructura de comunicación, los costes de comunicación pueden crecer descontroladamente. Este apartado incluye un número de sugerencias para mejorar el rendimiento en la comunicación. Utilice llamadas locales en vez de remotas. Con la aparición de EJB, la programación distribuida se ha hecho más sencilla. Debido a ello, se ha extendido el uso de EJB y, con él, el mal uso del mismo. Para evitar añadir sobrecarga inútil a la aplicación, se debería determinar para cada llamada a método si necesita ser distribuida. Si un objeto Java local puede encargarse de la llamada y ningún requerimiento de negocio impone una llamada remota, la llamada debe ser local. Si el objeto que recibe la llamada no necesita usar la seguridad del contenedor, el fondo común de instancias, las transacciones gestionadas por el contenedor y el objeto no necesita ser distribuido, entonces no utilice un EJB. Cuando realmente necesite un EJB, haga uso de las interfaces locales cuando sea posible. Las interfaces locales permiten pasar eficientemente parámetros por referencia y reducen la sobrecarga. Si el cliente que hace la llamada se encuentra en el mismo contenedor que el que la sirve puede hacer una llamada local con un rendimiento mucho mayor. Combine los datos. Para mejorar el rendimiento, es una buena práctica combinar los datos de una llamada a un método, tanto los parámetros como el resultado. Para ello, puede utilizar objetos. En vez de llamar a un método que tiene como parámetros una dirección y un número de teléfono, programe un método que tenga un objeto “Persona” como parámetro. Esta opción requiere un único objeto y mejora el rendimiento de las llamadas locales y distribuidas. En lo que respecta al resultado, como una llamada a método sólo puede devolver un único resultado, combinando los datos en un solo objeto puede devolver una mayor cantidad de datos en una llamada sencilla. Por lo tanto, se necesitan menos llamadas a métodos y se mejora el rendimiento. Agrupe las solicitudes en lotes Puede llevar todavía más lejos los beneficios de la combinación de datos, enviando varias solicitudes de datos similares en una única llamada. Realizando estas solicitudes de forma conjunta, sólo se necesita una única conexión al recurso. En el caso de una base de datos, se requiere una única conexión de base de datos y el SQL que se ejecuta necesita compilarse sólo una vez, ahorrando un número considerable de ciclos de cómputo. JDBC permite fácilmente agrupar las solicitudes en lotes, aumentando el rendimiento. Ponga los datos en una caché Otro factor importante para implementar un esquema eficiente de invocación de métodos es identificar datos que son costosos de obtener y que cambian con poca frecuencia. Usando una caché, un gran número de llamadas costosas y que consumen tiempo pueden sustituirse por rápidas lecturas de valores de la caché. Puede leer más sobre cachés en el apartado “Buenas prácticas de Diseño” en este mismo documento. 4. BUENAS PRÁCTICAS DE PRUEBAS La mayoría de los programadores tratan la etapa de pruebas (“testing”) como una obligación menor que deben cumplir para complacer a los superiores. Sin embargo, las pruebas de una aplicación no deberían tener lugar solamente después de su programación sino durante todo el ciclo de vida del desarrollo: Se deben definir los casos de prueba durante el análisis de requerimientos, permitiendo que los casos se apliquen a todos los requerimientos. Los programadores deberían escribir guiones de prueba antes, o como mínimo al mismo tiempo, que el código de aplicación. Este proceso asegura que cada línea de código tiene un caso de prueba y que los programadores programan la funcionalidad que se desea. Los casos de prueba deberían ejecutarse durante la integración, para verificar que no falla nada nuevo y que no han aparecido nuevos errores en el sistema. Las pruebas deberían producirse también en tiempo de despliegue, sobre el proyecto completado, para demostrar a todos los interesados que la aplicación cumple todos los fines preestablecidos de rendimiento y escalabilidad. En este apartado, se discutirán buenas prácticas que mejoran la eficiencia y calidad de las pruebas 4.1. Buena práctica número 9. Escriba primero los casos de prueba. Tradicionalmente, los casos de prueba se han ejecutado separadamente del código de aplicación. Este ineficiente proceso está cambiando rápidamente, conforme metodologías de desarrollo más ágiles requieren a los programadores escribir casos de prueba antes que el código de aplicación. Escribiendo primero los casos de prueba, se pueden obtener varios beneficios tangibles: Es más probable que los programadores comprendan la funcionalidad que se desea y que se centren más en implementar el código que satisface los requerimientos. A partir de este punto, las pruebas de integración pueden tener lugar sin impedimentos ni trabajo adicional, ya que los casos de prueba que existen aseguran el buen estado del sistema. Escribiendo primero los casos de prueba, la cultura de desarrollo da un mayor valor a las pruebas. Es menos probable que los programadores implementen código extraño que no se necesite para satisfacer requerimientos tangibles. Una de las razones más comunes por las que se dejan de ejecutar las pruebas es la falta de casos de prueba predefinidos. En las etapas tardías del proceso de desarrollo, las presiones sobre el tiempo frustran los esfuerzos de crear guiones completos de prueba. Sin embargo, aunque al principio pueda parecer un atraso, escribir primero los casos de prueba ahorra tiempo, ya que los programadores se centran en completar tareas relevantes y pasan menos tiempo en las pruebas de integración y de sistema que se producen más adelante en el ciclo de desarrollo. 4.2. Buena práctica número 10. Cree una plataforma de prueba. Las pruebas deberían ser un proceso automatizado que pueda ejecutar cualquier persona en cualquier momento. Esta regla asegura que las pruebas no se consideran una molestia, sino una parte integral del proceso de desarrollo. Con una única instrucción, un usuario o una aplicación debería poder ejecutar una variedad de juegos de prueba para determinar el estado global del código, así como el de cualquier parte del mismo. Para ello, los casos de prueba se pueden expresar en tres maneras: con clases a la medida que escriben a la salida estándar, con una plataforma comercial de pruebas o con JUnit. La mayoría de organizaciones que programen en Java utilizan JUnit debido a su destacada flexibilidad y facilidad de automatización. JUnit es una plataforma de pruebas de regresión de código abierto escrita en Java. Ofrece una forma de crear casos de prueba, agruparlos conjuntamente en juegos de prueba y hacer afirmaciones sobre los resultados. Esto permite una plataforma de pruebas completamente automatizada que puede ser ejecutada por cualquier persona en cualquier momento. 4.3. Buena práctica número 11. Automatice las pruebas Una vez se tiene una plataforma de pruebas, asegúrese de que no sea molesta. Los programadores no deberían tener que ejecutar manualmente las pruebas. Dada la gran importancia de las pruebas, deben ser algo que ocurra como una parte normal del proceso de desarrollo. Esto llevará a un número más alto de pruebas ejecutadas y a un menor número de errores. Para ayudar a añadir las pruebas al proceso de construcción, Ant tiene dos formas de ejecutar pruebas en JUnit: con una tarea que ejecuta clases Java o con una tarea específica para ejecutar casos de prueba en JUnit. Usando estas opciones, se pueden integrar eficazmente las pruebas al resto del proceso de desarrollo. Compruebe la calidad de nuestro desarrollo offshore. Aurum Solutions tiene experiencia en desarrollo offshore en J2EE y .NET de alta calidad y reducidos costos para empresas europeas y norteamericanas. Conozca nuestro desarrollo offshore haciendo clic aquí o en la página Web http://espanol.aurumsol.com/offshore.html. Cursos de Aurum Solutions relacionados con el tema de este artículo: “Buenas prácticas en arquitecturas J2EE” “Buenas prácticas en programación en Java” “Gestión de proyectos informáticos” “Análisis y diseño orientado a objetos” “Programación en n-capas” “Programación MVC” “Patrones de diseño” “Persistencia para Java” “Enterprise Javabeans” “Servlets y JSP” “Herramientas de pruebas para Java” |