Evitar los Obstaculos del Desarrollo con Struts

Puedes encontrar la versión original de este tutorial en Inglés en:

http://www.javaworld.com/javaworld/jw-04-2003/jw-0418-struts.html

A menos que hayas estado viviendo bajo una roca o en una caverna durante los últimos años, seguro que has oído hablar del marco de trabajo Struts. Struts es un iniciativa de código fuente abierto esponsorizada por la Apache Software Foundation y fue creado para mejorar el paradigma de diseño Modelo-Vista-Controlador (MVC) dentro de la capa de presentación de una aplicación Web. Struts implementa el patrón MVC usando el patrón Service to Worker (Servicio a Trabajador). Es una arquitectura bien diseñada y lucha por ser de acoplamiento ligero y ser altamente coherente.

Una de las tareas más duras a las que se enfrentan los arquitectos de aplicaciones empresariales es la creacción y mantenimiento de la capa de presentación. Los usuarios esperan interfaces gráficos de usuario de alta funcionalidad, robustos y elegantes. Así, la codificación de la capa de presentación consume muchos más recursos que la de la capa de aplicación. Además, la llegada de diferentes plataformas de clientes, como los teléfonos móviles y los PDAs han complicado todavía más esta situación.

Este artículo está elaborado sobre los problemas que le aparecen a los desarrolladores de aplicaciones Web que utilizan Struts y cómo resolverlos. Muchas de estas aproximaciones se pueden abstraer y aplicar a diferentes marcos de trabajo MVC como la especificación JavaServer Faces.

Los tópicos de esta explicación cubren aquellas áreas que representan la mayoría de los obstaculos que aparecen cuando se construyen aplicaciones J2EE utilizando Struts con BEA WebLogic Server.

Tomate dos Aspirinas y Llámanos por la Mañana

Sin lugar a dudas, Struts facilita el desarrollo y mantenimiento de interfaces de usuario para aplicaciones empresariales. Sin embargo, después de haber trabajado con Struts incluso en una aplicación sencilla, uno se da cuenta enseguida de la pesadilla que es el fichero struts-config.xml. Este fichero se puede convertir rápidamente en una bestia. Cuando se construye una aplicación empresarial, struts-config.xml puede crecer en exceso hasta 500 acciones de mapeo, haciéndose virtualmente inmanejable.

Nosotros recomendamos dos herramientas para ayudar a sobrellevar este dolor de cabeza. Primero, documentar el flujo de nuestro interface de usuario utilizando Microsoft Visio y StrutsGUI de Alien-Factory. StrutsGUI es una plantilla de Visio que nos ayuda a representar el flujo de un interface de usuario utilizando nomenclatura Struts. Hay una piedra preciosa oculta dentro del ítem de plantilla Struts: pulsando con el botón derecho esta opción, seleccionado Edit Title Properties, y luego seleccionando la opción Tools, podemos generar un fichero struts-config.xml basado en este diagrama. Por ejemplo, la sencilla aplicación mostrada en la siguiente figura genera el fichero struts-config.xml mostrado abajo:



<?xml version="1.0" encoding="ISO-8859-1" ?>

<!-- Struts Config XML - Sample Struts App -->
<!-- ===================================== -->

<!-- AutoGenerated from : c:devjavaworldappsample.vsd -->
<!-- AutoGenerated on   : 02-18-2003 23:05:47 -->
<!-- AutoGenerated by   : Struts GUI v2.11   (c)2002 Alien-Factory -->
<!--                    : See 'http://www.alien-factory.co.uk' for details -->

<!-- GET YOUR STICKY FINGERS OFF! i.e. Do not edit. -->

<!DOCTYPE struts-config PUBLIC
      "-//Apache Software Foundation//DTD Struts Configuration 1.0//EN"
      "http://jakarta.apache.org/struts/dtds/struts-config_1_0.dtd">

<struts-config>

  <!-- ====================== Form Bean Definitions =================== -->
  <form-beans>
  </form-beans>

  <!-- ================= Global Forward Definitions =================== -->
  <global-forwards>
  </global-forwards>

  <!-- ======================= Action Definitions ===================== -->
  <action-mappings>
    <action  path="/Login"
             type="com.agilquest.onboard.presentation.actions.LoginAction">
      <description>Authenticates and authorizes a user.</description>
    </action>
  </action-mappings>
  
</struts-config>

Para hacer más completo el diagrama de flujo del interface de usuario, recomendamos un paso adicional cuando se utiliza StrutsGUI. Dentro de nuestro documento StrutsGUI Visio, podemos enlazar fácilmente cada pagina JSP con su imagen real de la aplicación. Este truco no sólo ayuda en la documentación de la aplicación, sino que más importante, se convierte en una excelente herramienta para entrenar a nuevos desarroladores en el diseño de interfaces de usuario.

Otra herramienta que nos puede ayudar a manejar aplicaciones Struts es la Consola Struts creada por James Holmes. En esencia, proporciona las mismas facilidades que permiten llegar al mismo punto final que con StrutsGUI, pero difiere en la aproximación y las fortalezas. Ambas herramientas funcionan bien, y cualquiera de ellas mejorará el mantenimiento de aplicaciones basadas en Struts.

Ahora, ¿Dónde puse ese formulario?

El control de sesión de ActionForm puede ser aburrido. ¿Cuál debería ser la vida de un ActionForm? ¿Debería estar en el ámbito de la petición o en el ámbito de la sesión? Una solución a este problema es poner el ActionForm en la sesión durante el tiempo de vida que se le suponga a la funcionalidad que representa.

En dicho caso, ¿Cómo mantenemos genéricamente estos objetos ActionForm? ¿Quién asume la responsabilidad de limpiar estos objetos ActionForm cuando ya no son necesarios? Un escenario típico es cuando un usuario pasa de una funcionalidad a otra a través de un menú. En este caso, se deberían eliminar de la sesión los viejos objetos ActionForm y se deberían crear los nuevos objetos. Debería estar presente una clase Action centralizada, llamada MenuAction, que sólo trata con el paso por el menú. Esta clase Action borra de la sesión los objetos ActionForm redundantes. Luego reenvia al usuario a la nueva página donde se crean los nuevos objetos ActionForm.

Dando esto, ¿cómo mostraríamos los diferentes ítems de menú al usuario basándonos en su rol o permisos? Este menú también debería estar internacionalizado, y también pueden cambiar basándose en los permisos del usuario; es decir, si cambian los permisos, el menú debe cambiar de la forma apropiada. En dicha aproximación persisten los permisos del usuario. Cuando entra en el sistema, un MenuFactory crea menús partiendo de sus permisos. Para seguridad adicional, la clase MenuAction puede entonces autorizar la usuario permitiéndole proceder con la funcionalidad seleccionada.

Una regla del pulgar en el nombrado de objetos ActionForm en el fichero struts-config.xml es terminar el nombre del objeto con Form, simplificando con ello el mantenimiento de estos formularios en la sesión. Por ejemplo: ReservationForm, SearchUserForm, BankAccountForm, UserProfileForm, etc.

El siguiente código clarifica el control de ActionForm(s) ilustrando el paso por un menú genérico con los mapeos de Action:


public class MenuAction {

  public ActionForward perform(ActionMapping       _mapping,
                               ActionForm          _form,
                               HttpServletRequest  _request,
                               HttpServletResponse _response)
                                  throws IOException, ServletException {

    // Check end-user permissions whether allowed into the requested     
    // functionality 
    checkIfUserAllowedToProceed(_mapping, _form, _request, _response); 

    // Clean up the session object (this logic is in its own method)
    String formName = null; 

    HttpSession session = _request.getSession();
    Enumeration e = session.getAttributeNames();  

    while(e.hasMoreElements()) {
     
      formName = (String)e.nextElement();

      if (formName.endsWith("Form")){
        session.removeAttribute(formName);
      }    
    }

    // Now find out which functionality the end-user wants to go to
    String forwardStr = _request.getParameter("nextFunctionality");

    if (forwardStr != null && forwardStr.trim().length() > 0){
      return _mapping.findForward(forwardStr);
    }
    else {
      return _mapping.findForward("index");
    }
  }  
}

El siguiente mapeo de Action es un ejemplo de cómo implementar una acción basada en la selección de un menú:


<!-- A generic menu action that forwards the user from one 
     functionality to another functionality (after checking permissions)
-->
<action path="/menuAction"
        type="x.y.z.MenuAction"
        input="/menu.jsp">      
  <forward name="create_reservation" path="/actionResv.do"/> 
  <forward name="index"              path="/menu.jsp"/> 
  <forward name="add_person"         path="/actionPerson.do"/> 
  <forward name="logout"             path="/actionLogout.do"/> 
</action>

El ejemplo y el mapeo son auto-explicativos.

Dímelo de nuevo, ¿Cómo estámos Relacionados?

Cualquier página JSP puede tener muchos puntos de entrada y uno o más puntos de salida, dependiendo de la complejidad de la propia página. Reconcer estas relaciones es de suma importacia para entender y mantener la complejidad del interface de usuario. Nosotros hemos definido las relaciones entre una página JSP y una clase Action como:

  • Relación 1:1

    En una relación uno a uno, un usuario pasa de una página JSP a otra a través de una clase Action; esto facilita el acoplamiento fuerte entre una página JSP y una Action. La unica sobrecarga extra es un mapeo de Action en el fichero struts-config.xml. Esta única Action con su único mapeo de Action en el fichero struts-config.xml se puede utilizar para ir de una página a otra.

    Referenciar una página JSP directamente desde otra es una práctica pobre; no se pueden chequear los permisos del usuario que va a la página destino (si se aplica). Esto también implica problemas de mantenimiento. Para evitar estos problemas, siempre debemos ir desde una página JSP a otra utilizando una clase Action:

    
    <!-- Un Action genérico que envía la petición desde 
        una página JSP a otra -->
    <action path="/forwardAction"
            type="x.y.z.One2OneAction"
            input="/test1.jsp">      
      <forward name="continue"  path="/test2.jsp"/> 
    </action>
    
    
  • Relación 1:N

    Una relación un poco más complicada es aquella en la que una página JSP tiene varios puntos de salida pero sólo un punto de entrada, referida como una relación 1 a muchos. En este caso, siempre usamos una sóla clase Action para redigir a los diferentes objetivos. Esto asegurará que la clase Action chequea las diferentes situaciones y permisos antes de reenviar al usuario al destino.

    La unica sobrecarga extra es un mapeo de Action en el fichero struts-config.xml. Esto también facilita un mapeo 1:1 entre una página JSP y un mapeo Action.

    Abajo podemos ver el mapeo de Action que tiene un sólo punto de entrada pero varios puntos de salida:

    
    <!-- Un Action genérico que reenvia la petición 
        desde una página JSP a diferentes ramas 
        dependiendo del enlace seleccionado por el usuario -->
    <action path="/branchAction"
             type="x.y.z.One2NAction"
             input="/test1.jsp">      
      <forward name="target1"   path="/test2.jps"/> 
      <forward name="target2"   path="/test3.jsp"/> 
      <forward name="target3"   path="/someAction.do"/> 
    </action>
    
    
  • Relación N:N

    La relación más compleja, comumente referida como una relación muchos a muchos, es cuando una página JSP o una clase Action tiene varios puntos de entrada y varios puntos de salida. La relación N:N es una parte interesante y compleja que ocurre de forma frecuente en aplicaciones empresariales. Esta relación se utiliza principalmente cuando diferentes páginas JSP acceden a una página JSP común o a una clase Action común. Supongamos que el usuario ha llegado a un apágina JSP que es un hub (es decir, un página JSP a la que se puede llegar desde varias páginas JSP) y de algún modo quiere volver atrás o cancelar el flujo; el desarrollador se encuentra con el dilema de cómo enviar al usuario a la página correcta.

    Otro escenario es una clase Action que se comunica con la base de datos (usando diferentes funcionalidades o páginas JSP), y ocurre un error. Enviar al usuario de vuelta al lugar donde se originó o reenviarlo de forma apropiada, basándose en el lugar del que vino el usuario, requiere mucho cuidado. Los mapeos de struts-config.xml no serán muy útiles porque el campo de entrada es una página JSP fija o una clase Action. Se debería crear una arquitectura flexible donde un desarrollador pueda cambiar el flujo lógico sin volverse loco en el fichero struts-config.xml. Aquí es donde entra la relación N:N. Implementando un interface con la flexibilidad de enviar al usuario o bien al origen o al destino, los valores pueden cambiar fácilmente.

    Creamos la siguiente clase para implementar el interface:

    
    public class N2NAction {
      public ActionForward perform(ActionMapping      _mapping,
                                   ActionForm         _form,
                                   HttpServletRequest _request,
                                   HttpServletResponse _response)
                                      throws IOException, ServletException {
    
        N2NInterface if = (N2NInterface)_form;
    
        //Ejecuta aquí alguna funcionalidad de negocio
        try{
          //Ha tenido éxito la lógica de negocio?
                   
        }
        catch(Exception e){
    
          //Indica un fallo
          return _mapping.findForward(if.getSource()); 
        }
    
        //Indica éxito
        return _mapping.findForward(if.getDestination());            
    
      } 
    
    }
    
    

    Abajo podemos ver el mapeo de Action que tiene varios puntos de entrada y varios puntos de salida:

    
    <!-- Un Action genérico que envía la petición desde 
        una página JSP a otra -->
    <action  path="/sourceAndDestinationAction"
             type="x.y.z.N2NAction"
             input="/test1.JSP">      
      <forward name="source1"          path="/source1.JSP"/> 
      <forward name="source2"          path="/source2.JSP"/> 
      <forward name="source3"          path="/someAction.do"/> 
      <forward name="destination1"     path="/destination1.JSP"/> 
      <forward name="destination2"     path="/destination1.JSP"/> 
      <forward name="destination3"     path="/destination2.JSP"/>
    </action>
    
    

    Un hiperenlace podía ser algo como esto:

    
       <a href="srcAndDestAction.do?source=source1&destination=destination1">click me</a>
       <a href="sreAndDestAction.do?source=source2&destination=destination2">click me too</a>
    
    

    Por defecto, todos los ActionForms deben soportar estos tres tipos de relaciones (normalmente utilizando interfaces). Usando clases Action genéricas, podemos navegar fácilmente por cualquier lugar del flujo del interface de usuario.

¡Que dolor de Espalda!

Una buena aproximación para diseñar la capa de presentación es diseñar por funcionalidades. Por ejemplo, mientras hacemos una reserva en un sistema de reservas, empaquetar todas las clases Action relacionadas en un paquete como

com.companyname.productname.presentation.reservation.mak

es una mejor aproximación. Ni que decir tiene que el empaquetado también se aplica a las clases ActionForm. Los formularios deberían estar en la sesión durante el tiempo de vida de la funcionalidad. Esto nos asegura que los datos necesarios para toda esta funcionalidad están en el propio objeto form. Así, el usuario podrá navegar a cualquier página de la funcionalidad y encontrar que se muestran los datos correctos en esa página. Entonces el usuario podrá actualizar los valores antes de grabar los datos finalmente.

Ahora,nos aparece un dilema interesante: ¿qué sucede cuando el usuario envía los datos, utiliza el botón Back del navegador, hacer algunos cambios y los envía otra vez? Por ejemplo, después de crear una reserva en la base de datos, el usuario vuelve atrás e intenta enviar los mismos datos de la reserva. La capa de presentación debe capturar este error antes de que la capa de aplicación tenga la oportunidad de completarse. Una forma de tratar esta situación es crear un testigo antes del envío, chequear la validez del testigo después del envío, e inmediatamente cambiar el valor del testito -- para que el usuario no pueda utilizar el botón Back para enviar los mismos datos otra vez.

Un problema que tiene esta aproximación es el manejo de los testigos: por ejemplo, si el usuario intenta grabar los datos y falla, el valor del testigo ya ha cambiado. Si es así, el usuario no puede modificar los datos ni reenviarlos sin restablecer el testigo. La clase Action correspondiente no permitirá el envío. Exactamente ¿cuándo debería restablecerse el testigo? Un usuario podría haber pasado por seis páginas, luego recibir un error mientras envía los datos, y ser dirigido a cualquiera de las seis páginas.

Para solventar este problema, creamos el form y almacenamos el valor del testigo en la sesión, tan pronto como el usuario solicite una funcionalidad particular. Antes de grabar los datos, el usuario puede utilizar el botón Back tantas veces como quiera y hacer los cambios que desee. Una vez que el usuario envíe esos cambios, se restablece el testigo sólo después de que la grabación de datos haya tenido éxito. Si la grabación falla, el usuario puede pulsar el botón Back e ir a cualquier página JSP donde se haya originado el fallo, modificar los datos, y reenviar. Una vez que la grabación haya tenido éxito, puede cambiar el valor del testigo. Los propios objetos ActionForm pueden contener este valor de testigo (que se puede configurar programáticamente). O después de la primera grabación, se puede desactivar el botón Submit, y no permitir que el usuario reenvie los datos hasta que se suceda algo. Sugerimos que cada ActionForm maneje su propio testigo, una aplicación podría usar más un de objeto ActionForm para tratar con una funcionalidad particular.

¿Quién eres tú y qué haces aquí?

Una aplicación empresarial debe estar diseñada para soportar simultáneamente varios esquemas de autentifiación. Esto es especialmente cierto para sofwate de ISVs (independent software vendors). Por ejemplo, asumamos que los requerimientos de una aplicación son una simple firma, un reto usuario-password, una autentificación de código de barras, o una autentificación de scan de huella digital, con la futura posibilidad de una autentificación por voz. JAAS (Java Authentication and Authorization Service) es un mecanismo de autentificación conectable que es muy útil para este propósito. Por defecto, Struts 1.1 soporta JAAS. JAAS también se puede utilizar con Struts 1.0 con muy buenos resultados. Un buen diseño de herencia para autentificación facilitará el uso de diferentes clases action para autentificaciónm cada una de las cuales tiene su único y propio mapeo Action. Este tipo de diseño facilitará el soporte simultáneo de diferentes autentificaciones. Si se necesita añadir un nuevo mecanismo de autentificación, simplemente creamos un nuevo mapeo de Action en el fichero struts-config.xml con una clase Action que soporte esa autentificación.

¿A dónde vamos desde aquí?

Los controladores de flujo se han diseñado para restringir o guiar al usuario a lo largo de una aplicación y son prácticos para asegurar que el usuario sigue un flujo particular. Basar los controladores de flujo en los roles del sistemas es muy útil. Siempre que un usuario intente acceder a una funcionalidad, el controlador de flujo se asegura de que el usuario tiene los permisos correctos para ir por un flujo particular. También, en caso de una excepción indeseada, se puede guiar al usuario a la página apropiada. El mejor lugar para poner un controlador de flujo es la superclase. Sólo después de que se haya satisfecho el controlador de flujo se puede permitir al usuario realizar una acción particular.

La siguiente figura representa un sencillo modelo de herencia que se puede utilizar para aprovecharnos de las ventajas de un controlador de flujo combinado con la autentificación del usuario:

Errar es humano...si fueramos cyborgs

El marco de trabajo Struts proprociona una infraestructura de manejo de errores mediante las clases ActionError y ActionErrors. Un ActionError exactamente es—un error que ha ocurrido en una clase Action o ActionForm, o ha sido lanzado desde la capa de aplicación. La clase ActionError normalmente está constuida utilizando una sola clave o utilizando una pareja clave/valor. Si nuestra aplicación está internacionalida, se puede utilizar la clave como una búsqueda dentro de la base de datos de recursos de mensajes internacionalizados. Si no importa la internacionalización, entonces los valores de reemplazo se pueden utilizar para mostrar un mensaje de error. Se pueden reservar hasta cuatro posiciones en un objeto array, cada una contiene una parte independiente del mensaje de error. Estos valores se construyen de forma muy similar a la clase MessageFormatter.

La clase ActionErrors, que extiende la clase ActionMessage, es una collection de clases ActionError con un sólo método público: add(java.lang.String property, ActionError error). El segundo parámetro de la firma de este método es bastante sencillo: el error real (ActionError) que ha ocurrido. El parámetro property se utiliza para validación a nivel de campo asociando el mensaje de error a un campo específico. Como el mensaje de error está asociado con un campo específico, podemos localizar el mensaje fácilmente en las cercanías del campo en cuestión o en cualquier otro lugar que tenga más sentido. Por ejemplo, si tuviéramos una validación a nivel de campo sobre un identificador de login de un usuario y se lanzara un error en la clase LoginAction, usaríamos el siguiente fragmento de código:

ActionErrors errors = new ActionErrors();
errors.add("logiinID", new ActionError("loginID.invalid"));
saveErrors(_request, errors);
return (new ActionForward(_mapping.getInput()));

El string "loginID.invalid" es la clave para el valor de un string internacionalizado en la base de recursos de mensajes. Para mostrar el mensaje de error, en nuestra página login.jsp, utilizamos el siguiente código HTML:


<font color="red"><html:errors property="loginID"/></font>

Con este entendimiento específico de la facilidad de Struts para manejar excepciones y cadenas de excepciones, se necesita una aproximación más general para manejar errores a través de las arquitecturas de aplicaciones empresariales. Algunos de los principios más importantes para el trabajo con excepciones en una aplicación empresarial son:

  • Desarrollar un árbol de clases Exception
  • Independizar las excepciones de usuario de las del sistema.
  • Darle a las excepciones un contexto, un tipo y un nivel de seguridad.
  • Independizar el manejo de excepciones y el logging a la aplicación.
  • Crear una facilidad para el encadenado de excepciones.
  • Externalizar los strings de las excepciones para su internacionalización.

Pruebas!, de esto están hechos los usuarios!

Aunque muchos desarrolladores lo han visto como el aspecto más glamuroso del desarrollo de softwate, la prueba de aplicaciones empresariales es vital para su éxito general. Múchos métodos ágiles, como la programación extrema (XP), sitúan las pruebas como el frente principal del desarrollo de aplicaciones empresariales. Este enfasis realmente es muy refrescante.

En las pruebas del lado del servidor se utilizan principalmente dos estrategias: mock object (MO) e in-container (IC). Las pruebas Mock object (objeto maqueta) básicamente extienden un comportamiento "tropiezo" y es auto-explicativo. En esencia, un desarrollador es responsable de maquetar las clases que definen el interface para ser comsumidas por las clases que se están probando. Usando esta aproximación el desarrollador imita al contenedor. Este tipo de testeo también es conocido como endo-testeo (testeo interno) debido al acto de situar los test dentro de una clase en un entorno controlado.

El testeo In-container es una estrategia donde también se utiliza en el proceso de la prueba el contenedor real que se utilizará en el entorno de producción . Los casos de prueba se deben incorporar al servidor, como mínimo, en un proceso de construcción nocturno utilizando Ant..

Una forma efectiva de probar clases Java es utilizar JUnit. Esta es una excelente herramienta para probar nuestros JavaBeans normales. Otra gran herramienta de código abierto es Cactus. Cactus, es una extensión de JUnit, prueba código del lado del servido al nivel de unidad. Cactus soporta la aproximación in-container para pruebas del lado del servidor. Ambas herramientas son imprescindibles y no deberían faltar en cualquier equipo de desarrollo de aplicaciones empresariales que busque la calidad.

La estrategia generalmente aceptada para pruebas en Struts es probar las clases de la capa de presentación a través de la capa de aplicación. Si seleccionamos una aproximación u otra depende de las necesidades de nuestro grupo y del nivel de confort. Para pruebas automáticas con Struts se creó una extensión de JUnit llamada StrutsTestCase que soporta tanto MO como IC.Testear las clases Action y ActionForm usando StrutsTestCase es un parte de toda la estrategia de pruebas general que debería incluir las herramientas de pruebas regresivas descritas aquí. Se pueden utilizar otras herramientas como Apache JMeter para pruebas de stress/carga.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP