OZ Ejemplo Cajero ATM

¿Que vamos a desarrollar?

Una aplicación que simule el funcionamiento de un cajero automático, mostrando el uso de múltiples frameworks Java para crear un sistema modular y extendible.

Requisitos

  • Apache Maven >= 3.X
  • Java JDK >=  1.6
  • Netbeans >= 6.5
  • Base de Datos MySQL

Análisis

Supongamos que tenemos el siguiente requerimiento por parte del cliente o del analista:

Crear un cajero que permita realizar dos operaciones, Retirar efectivo y hacer Transferencias a otro Banco.

Veamos como se vería en un diagrama de casos de uso:

El usuario unicamente va a realizar dos operaciones retiro y transferencia.

Bien ahora la base de datos, vamos a utilizar este script para generarla.  Ejecutas el archivo en tu gestor favorito y te  creará el esquema llamado OZ_TEST, con las siguiente tablas:

La tabla CUSTOMER almacena los datos del cliente y ACCOUNT el saldo que tiene disponible.

Las tablas contienen los siguientes datos:

# CUSTOMER
INSERT INTO `OZ_TEST`.`CUSTOMER` (`id_customer`, `name`) VALUES (1, 'Alberto Sánchez');

# ACCOUNT
INSERT INTO `OZ_TEST`.`ACCOUNT` (`id_account`, `balance`, `id_customer`) VALUES (1, 10000000.0, 1);

Diseño

Bien ahora que ya sabemos lo que tenemos que hacer vamos a crear nuestro diseño para resolver el problema, comencemos por la interface gráfica, necesitamos una pantalla como esta:

Nota: (Imaginación de Beto de como se vería la pantalla…. y si, me faltaron unas ‘S’ en el aviso jajaja :p)

El proceso para utilizarla sería el siguiente:

  1. Escribes el número del cliente y el importe por el cual quieres realizar la operación.
  2. Seleccionas una operación.
  3. El resultado de cualquiera debe mostrar un mensaje satisfactorio o incorrecto.

Bien ahora que ya tenemos la pantalla vamos a diseñar los procesos en uml, si crearamos una clase se vería de la siguiente forma:

[ Diagrama UML ]

La clase Cajero tiene dos métodos retirar y transferir, cada uno recibe dos argumentos, un Integer para el id del cliente y un double para el importe.

¿Se ve sencillo verdad? con esto podríamos resolver el problema y todos felices n_n…

PUES NOOOOO!!!! aunque esto funcione es lo peor que podemos hacer, y ¿por que?….  porque terminamos creando código aislado,  difícil de mantener, poco extendible y que en un futuro puede darnos tremendos dolores de cabeza. Por otro lado si utilizamos buenas prácticas podemos ganar ENORMES beneficios para resolver nuestro problema, vamos a ver como podemos manejar esto, primero tenemos que aplicarle dos sencillas reglas:

  1. El código debe ser Altamente Cohesivo, significa que la clase debe ser re-utilizable y contener solo aquellos procesos para los que fue creada, además los metodos deben hacer exactamente lo que dicen que hacen.
  2. Las clases y componentes deben tener un Bajo Acoplamiento, de ser posible el código NO debe depender de otros elementos para funcionar, pero atención con esto debe ser lo más independiente posible, insistó lo más que se pueda.

¿Y que ganamos si cumplimos esto?

  • Código flexible, fácil de modificar y mantener
  • Fácil de escalar o extender
  • Código portable y sobre todo re-utilizable por otros componentes
  • Fácil de probar!! <— muy importante
  • Reducimos el tiempo de desarrollo ( más tiempo para nuestra vida personal y youtube =D!! )
  • El código es auditable para controles de calidad (QA)
  • Y muchas, muchas más… Integración continua, Liberación continua, Control de versiones automático, Documentación automática, etc…

Suena bien… ¿ verdad? Vamos a verlo poco a poco…

A partir de aquí vamos a explicar de forma sencilla algunos conceptos que necesitamos, sin embargo y como sugerencia, sería bueno que te familiarizaras con los siguientes temas y que le dediques algo tiempo para comprenderlos. Para nuestro ejemplo NO es necesario que los domines porque vamos a explicarlos brevemente, pero sería recomendable que los tengas presentes:

Todos ellos son excelentes fundamentos para el desarrollo orientado a objetos ya sea en Java o cualquier otro lenguaje como C#, Python, PHP, etc.

Comencemos por aplicar las dos reglas que mencionamos al diseño de nuestra clase Cajero:

Cohesión

Debe ser re-utilizable y contener solo aquellos procesos para los que fue creada y  los métodos deben hacer exactamente lo que dicen que hacen

Si vemos nuestra clase cumple con esto:

  • Contiene solo los procesos para los que fue creada: SI
  • Los métodos hacen lo que dicen: SI
  • El código es re-utilizable: NO

Si por ejemplo, un día queremos crear un nuevo cajero donde SOLO se realicen retiros no podría utilizar el código anterior, porque todo está concentrado en una sola clase, es decir cualquier cajero creado a partir de esta clase va a tener obligatoriamente los dos procesos.  Así que este diseño NO es eficiente porque no podemos utilizarlo para crear nuevos cajeros, entonces creo que sería buena idea separar las operaciones de la siguiente manera:


Ahora se ve un poco mejor, en lugar de una clase tenemos tres:

  • La clase Cajero tiene dos atributos (el objeto retiros y transferencias ) cada uno de estos objetos es utilizado para hacer referencia a la operación que le corresponde  y tiene dos métodos, retirar y transferir.
  • La clase Retiros contiene el proceso para retirar
  • La clase Transferencias contiene el proceso para transferir

Como se puede ver con este diseño ya podemos crear nuevos cajeros capaces de re-utilizar los procesos por separado, por ejemplo un cajero que solamente efectue retiros:

A mi parecer nuestro código ya es más Cohesivo  porque cumple con los puntos que mencionamos anteriormente:

  • Contiene solo los procesos para los que fue creada: SI
  • Los métodos hacen lo que dicen: SI
  • El código es re-utilizable: SI

Continuemos con la siguiente regla.

Acoplamiento

 De ser posible el código no debe depender de otros elementos para funcionar

Este es un punto algo contradictorio con la regla anterior, si vemos nuestro Cajero ahora DEPENDE de dos clases para funcionar (Retiros y Transferencias) pero vamos a ser realistas UN CAJERO “necesita realizar alguna operación ya sea de Retiros y/o Transferencias” entonces no podemos inventar el hilo negro y tampoco vamos armar una nave espacial con palitos de madera, así que en lo posible esto se ve razonable

Existen tres niveles de acoplamiento:

  • Cero Acoplamiento: El código no tiene dependencias y puede operar por si mísmo
  • Bajo Acoplamiento: El código tiene algunas dependecias y puede operar con algunos de ellos
  • Alto Acomplamiento: El código tiene muchas dependencias  y NO puede operar sin alguno ellos

Para nuestro Cajero podemos decir que tiene un bajo Acoplamiento, porque tiene dos dependencias una a la clase Retiros y otra a Transferencias, sin embargo estas clases son independientes y no necesita de otros componentes, además puede operar con alguno de ellos por eso el nivel de acoplamiento es bajo.

Por ejemplo si nuestro cajero tuviera mas de 20 procesos que a su vez estan ligados a otros sub procesos se convierte en un Alto Acoplamiento, por otro lado si el cajero no dependiera de ningún componente podemos decir que tiene Cero Acoplamiento (que es nuestro diseño inicial estaba concentrado todo en una sola clase).

¿Entonces que es mejor? bajo, alto o cero acoplamiento… definitivamente es mejor el bajo, ya qué rara vez te encontrarás con clases que tengan cero acoplamiento, porque son clases altamente especializadas y muy frecuentemente te encontrarás con clases que tengan alto acoplamiento, porque son clases que tienen muchos componentes (esto refleja un mal diseño)… y cuidado porque estas son las peores…

Esta regla es algo difícil de aplicar porque hay que ser muy objetivos y realistas con lo que necesitamos, por eso hay que tener en mente que “no hay cajero sin alguno de estos procesos” porque podemos divagar mucho y perdernos.

Pero aún hay más cosas que tenemos contemplar en nuestro diseño con el Acoplamiento, así que veamos como puede ser afectado por cosas que no alcanzamos a visualizar o dimensionar en el primer acercamiento…

Polimorfismo

Imaginemos que hoy nuestro diseño del cajero se conecta directamente a nuestra base de datos, pero mañana cambian las políticas y nuestro usuario o analista se acerca y dice:

“Por políticas de seguridad los cajeros externos al banco se van a conectar remotamente por un Web Service y los cajeros de las sucursales de forma directa a la base de datos”.

¬¬ qué!! no me jod… haber aterricemos un poco “la pólitica”, no quieren que se cambie nada en el proceso todo debe funcionar igual así como esta diseñado, sin embargo solo quieren que los cajeros externos se conecten de otra forma… eso es todo.

Ok, cosas como esta son las que no alcanzamos a percibir cuando desarrollamos…  si vemos nuestro diagrama hace exactamente lo que queremos y no debemos modificar ninguno de los procesos:

Sin embargo no pensamos que los cajeros se iban conectar de forma diferente, entonces necesitamos una manera de definir nuevos cajeros que soporten esta capacidad. En el desarrollo Orientado a Objetos y en  Java esto se puede realizar con el Polimorfismo.

El polimorfismo es una forma de crear objetos abstractos que en escencia son definidos de forma general pero tienen un comportamiento diferente de acuerdo a su implementación.

Por ejemplo imagina dos pelotas, una de Soccer y otra de Basket Ball, en esencia las dos son pelotas y sirven para jugar, pero se comportan de forma diferente ¿cierto?, ya sea por tamaño, el peso, compresión, textura etc…

Entonces puedo decir que tengo una Pelota de Soccer y una Pelota de Basket. La interface sería la Pelota y las implementaciones son una de Soccer y una  de Basket.

Aquí entra la analogía con nuestro cajero, porque tenemos un Cajero con conexión remota y un Cajero con conexion local, ambos tienen los mismos procesos pero se comportan de forma diferente ¿ves la relación?

El polimorfismo sirve para ¡eso! te ayuda crear un objeto abstracto en el cual se define lo que quieres que haga, pero su comportamiento es realmente determinado por la clase que la implementa (es decir la clase que ejecuta la acción).

La regla del polimorfismo es sencilla:

Debes envolver tus objetos en algo Abstracto para poder utilizarlos. Ya sea una clase abstracta o interface.

Veamos nuestro diagrama aplicandole polimorfismo, creamos la interface Cajero que es abstracta y creamos dos implementaciones a partir de ella, el cajero local CajeroBancario y el cajero remoto CajeroOtzo:

  • Cajero, es abstracto y define los métodos (retirar y transferir)
  • CajeroBancario, es una implementación de Cajero, el cual esta obligado a implementar los métodos de la interface retirar y transferir, este se va a concetar de forma directa.
  • CajeroOtzo, es otra implementación de Cajero  y es el que podemos conectar de forma remota con el webservice.

Con esto cumplimos la regla,  nuestros cajeros están siendo envueltos por la interface Cajero que es 100% asbtracta. Y aquí viene un buen tip:

El desarrollar bajo interfaces garantiza un Bajo Acoplamiento y una Alta Cohesión por eso es MUY recomendable desarrollar con clases e interface abstractas

Ahora si en un futuro necesitamos nuevos cajeros, el código tiene el soporte para definirlos, pero hay que ser objetivos con lo que queremos, NO se trata de cubrir todos los casos posibles, se trata de qué el código pueda adaptarse en el futuro.

Bien ya explicado esto vamos a llevar el polimorfismo a nuestro diseño, veamos como quedaría.

Así es como lo dejamos:

Aplicando el polimorfismo se convierte a esto:

Como menciona la regla encapsulamos los objetos con clases abstratctas en este caso interfaces.

Nota, para mi: aún puedes ser dentista decias… pero NO!… querías sistemas ¿verdad? ¬¬

Veamos los cambios:

  1. Se crearon las interfaces Cajero, Retiros y Transferencias, esto va a permitir que podamos intercambiar fácilmente las implementaciones de cada una.
  2. Las clases CajeroBancario, RetirosImpl y TransferenciasImpl son las clases que ya teniamos pero apartir de ahora les vamos a llamar implementaciones.

Aun con estas modificaciones seguimos respetando todas las reglas que ya mencionamos.

Vamos a ver como sería el diseño de la conexión remota con el Web Service:

¿Es muy similar al anterior cierto? lo único que cambia son las implementaciones: CajeroOtzo, RetirosWsImpl, ransferenciasWsImpl Como se puede ver ya tenemos una estructura que soporta la creación de diferentes cajeros.

Ahora bien si vemos esto desde la perspectiva de usuario final SON CAJEROS y no me interesa como le hagan para ejecutar sus procesos yo solo se que los hacen y punto.

Eso es lo que las interfaces hacen, permiten ocultar la implementación y el comportamiento de una clase, lo único que ves son cajeros pero internamente sus implementaciones pueden hacer que se comporte de forma diferente.

Si nos piden crear un cajero blindado lo único que debemos hacer es generar nuevas implementaciones de las interfaces sin afectar lo que ya teniamos hecho, por ejemplo:

La ventaja de todo este modelo es que podemos re-utilizar los componentes en diferentes implementaciones y agregar funcionalidad especifica para cada uno, como el caso que mencionamos de la conexión del webservice, un cajero directo y otro remoto… ahora ¿puedes ver porque todo en una clase NO es lo más adecuado?. El código es mucho mas flexible que antes y esta preparado para modificaciones en el futuro.

Lo que quiero compartir aquí es precisamente la DIMENSIÓN y el ALCANCE que tenemos que contemplar, porque de un requerimiento tan simple como este se pueden derivar muchísimos casos y no se trata de cubrir todos, como dije antes no vamos a inventar el hilo negro y NO somos videntes para determinar todos los casos posibles, pero SI podemos dejar que el código sea flexible y mantenible para futuros desarrollos.

Bueno vamos a dejar el diseño hasta aquí, porque es un tema de percepción y puede ser tan amplio o tan corto como uno lo deseé, por eso debemos tener en mente bien claro lo que queremos hacer. Ahora vamos a regresar a concentrarnos en nuestro ÚNICO caso, hacer un solo cajero!, cuyo diseño final es  el siguiente:

Tenemos un Cajero con una implementación llamado CajeroBancario el cual tiene como atributos dos objetos, retiros y transferencias que a su vez permiten ejecutar los procesos correspondientes de cada uno.

Desarrollo, implementación y pruebas

Hora de hacer ¡código! :D…  una vez que domines esto en 15 min puedes hacer todo el ejercicio, claro siempre y cuando si tenemos un buen analisis y diseño, el desarrollo es la etapa más sencilla (lo difícil es que esto suceda en la vida real, no todos hacen su trabajo como deben).

Creamos el proyecto, se puede generar de dos formas y pueden utilizar cualquiera, yo recomiendo la del IDE que es la más comoda:

1. Primer forma : Con el IDE

  1. Abrimos Netbeans,  en el menú principal seleccionas File > New Project, y en la ventana en Categories seleccionas Maven y en Projects,  Java Application:
  2. Ahora escribes los datos del proyecto, y posteriormente  seleccionas Finish.
    • Project Name: atm
    • groupId: com.oz
    • version: 2.0
    • package: com.oz.atm

  3. Al final te va a crear la siguiente estructura:

2. Segunda forma: Desde consola

  1. Abres una terminal y escribes el siguiente comando que mostrará en pantalla la lista con todos los templates de maven.
    mvn archetype:generate
  2. Ahora debemos elegir la estructura para generar el proyecto,  seleccionamos la 224 “maven quickstart” en su versión 1.1.  Debemos escribir los datos para identificar el desarrollo  con los siguientes valores:
    • groupId: com.oz
    • artifactId: atm 
    • version: 2.0
    • package: com.oz.atm
    Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): 224: 
    Choose org.apache.maven.archetypes:maven-archetype-quickstart version: 
    1: 1.0-alpha-1
    2: 1.0-alpha-2
    3: 1.0-alpha-3
    4: 1.0-alpha-4
    5: 1.0
    6: 1.1
    Choose a number: 6: 
    Define value for property 'groupId': : com.oz
    Define value for property 'artifactId': : atm
    Define value for property 'version':  1.0-SNAPSHOT: : 2.0
    Define value for property 'package':  com.oz.atm: : 
    Confirm properties configuration:
    groupId: com.oz
    artifactId: atm
    version: 2.0
    package: com.oz
     Y: : Y
  3. Al final creará la siguiente estructura de carpetas:

Configurar las dependencias

Ya que tenemos la estructura del proyecto vamos a agregar los frameworks que vamos a utilizar:

  • SLF4J y Logback: Para controlar el log del sistema
  • JUnit: Para las pruebas unitarias
  • Hibernate: Para controlar el Acceso a base de datos y manejar persistencia
  • Spring: Permite realizar DI y IoC y gersitonar el ciclo de vida de los beans.

Editas el archivo pom.xml del proyecto y escribes el siguiente contenido:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.oz</groupId>
  <artifactId>atm</artifactId>
  <version>2.0</version>
  <packaging>jar</packaging>

  <name>atm</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

      <!--  Loggers -->
      <slf4j.version>1.6.1</slf4j.version>
      <logback.version>1.0.6</logback.version>

      <!-- Testing -->
      <junit.version>4.9</junit.version>
      <c3po.version>0.9.1.2</c3po.version>
      <commons-dbcp.version>1.4</commons-dbcp.version>

      <!-- Spring, Hibernate -->
      <spring.framework.version>3.1.1.RELEASE</spring.framework.version>
      <springframework.version>3.0.2.RELEASE</springframework.version>

      <hibernate.version>3.6.10.Final</hibernate.version>

      <!-- JDBC Drivers -->
      <hsqldb.version>2.2.8</hsqldb.version>
      <mysql.version>5.1.10</mysql.version>

        <!--  Plugins or utils  -->

      <commons-lang.version>2.6</commons-lang.version>

  </properties>

  <dependencies>

      <!-- Loggers -->

      <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-api</artifactId>
          <version>${slf4j.version}</version>
      </dependency>

      <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>jcl-over-slf4j</artifactId>
          <version>${slf4j.version}</version>
      </dependency>

      <dependency>
          <groupId>ch.qos.logback</groupId>
          <artifactId>logback-classic</artifactId>
          <version>${logback.version}</version>
          <exclusions>
              <exclusion>
                  <artifactId>slf4j-api</artifactId>
                  <groupId>org.slf4j</groupId>
              </exclusion>
          </exclusions>
      </dependency>
      <dependency>
          <groupId>ch.qos.logback</groupId>
          <artifactId>logback-core</artifactId>
          <version>${logback.version}</version>
      </dependency>

      <!-- Testing -->
      <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>${junit.version}</version>
          <scope>test</scope>
      </dependency>

      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-test</artifactId>
          <version>${spring.framework.version}</version>
          <scope>test</scope>
      </dependency>

      <!-- Spirng Framework-->

      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-context</artifactId>
          <version>${spring.framework.version}</version>
          <exclusions>
              <exclusion>
                  <artifactId>commons-logging</artifactId>
                  <groupId>commons-logging</groupId>
              </exclusion>
          </exclusions>
      </dependency>

      <!-- Spring ORM -->

      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-tx</artifactId>
          <version>${spring.framework.version}</version>
      </dependency>
      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-orm</artifactId>
          <version>${spring.framework.version}</version>
      </dependency>

      <!-- Hibernate -->

      <dependency>
          <groupId>org.hibernate</groupId>
          <artifactId>hibernate-core</artifactId>
          <version>${hibernate.version}</version>
          <exclusions>
              <exclusion>
                  <artifactId>slf4j-api</artifactId>
                  <groupId>org.slf4j</groupId>
              </exclusion>
          </exclusions>
      </dependency>

      <!-- Hibernate JPA-->
      <dependency>
          <groupId>javax.persistence</groupId>
          <artifactId>persistence-api</artifactId>
          <version>1.0</version>
      </dependency>
      <dependency>
          <groupId>javax.transaction</groupId>
          <artifactId>jta</artifactId>
          <version>1.1</version>
          <type>jar</type>
          <scope>compile</scope>
      </dependency>
      <dependency>
          <groupId>org.hibernate</groupId>
          <artifactId>hibernate-entitymanager</artifactId>
          <version>${hibernate.version}</version>
      </dependency>

      <!-- JDBC Connector's-->

      <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <version>${mysql.version}</version>
      </dependency>

      <dependency>
            <groupId>org.hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
            <version>${hsqldb.version}</version>
            <scope>test</scope>
        </dependency>

      <!-- Connection Pools -->
      <!-- C3po -->
      <dependency>
          <groupId>c3p0</groupId>
          <artifactId>c3p0</artifactId>
          <version>${c3po.version}</version>
      </dependency>

      <!-- Apache Connection Pool -->
      <dependency>
          <groupId>commons-dbcp</groupId>
          <artifactId>commons-dbcp</artifactId>
          <version>1.4</version>
      </dependency>

  </dependencies>

  <build>
      <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                    <encoding>UTF-8</encoding>

                </configuration>
            </plugin>
        </plugins>
  </build>
</project>

Con esto declaramos las librerias que vamos a utilizar.

1. Configuración de Logger (SLF4J y Logback) ** Buena Práctica😀

Para administrar nuestros logs utilizamos SLF4J que es un framework que permite encapsular y gestionar múltiples loggers en uno. Algunos frameworks como Spring trabajan con varios sistemas de log como el Apache Commons Login, Log4J, Logger de Java, etc… Para poner orden a todos ellos SLF4J establece un mecanismo sencillo y centralizado para dar soporte a todos los componentes, para ello utilizamos una implementación especial llamada Logback.

Para configurarlo creamos el archivo logback.xml en la carpeta src/main/resources :

Y lo editas con el siguiente contenido:

<?xml version="1.0" encoding="UTF-8"?>

<configuration>

    <!--
    Extrae el log del servidor y lo almacena en arcivos separados por dia
    yyyy-MM-ww          : Por semana
    yyyy-MM-dd          : Por dia
    yyyy-MM-dd_HH       : Por hora
    yyyy-MM-dd_HH-mm    : Por minuto
    -->

    <!--        Habilita la salida estadandar via consola    -->

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <layout class="ch.qos.logback.classic.PatternLayout">
            <Pattern>%d{HH:mm:ss.SSS} | %-5level | %logger{36} - %msg %n</Pattern>
        </layout>
    </appender>

    <!--
            Levels:
                TRACE, DEBUG, INFO, WARN, ERROR, ALL or OFF.
    	         Note that the level of the root logger cannot be set to INHERITED or NULL.
		<logger name=".*" level="debug"/>
        -->

    <!-- <logger level="debug" name="com.oz.atm" />-->

    <root level="info">
        <appender-ref ref="STDOUT"/>
    </root>

</configuration>

Con eso ya queda configurado nuestro sistema de logger.

2. Configuración de JUnit ** Buena Práctica😀

JUnit es un framework para realizar pruebas y es un punto muy importante, porque nos ayuda a garantizar que el código es funcional de manera automática, esto permite probar ciertos componentes de un sistema sin la necesidad de probar todo. Nos ayuda  a reducir el tiempo de desarrollo y a encontrar bugs de una forma aceleráda.

En realidad no necesitamos configuración pero si vamos a aplicar un sencillo truco, en el paquete test/java/ generas el paquete com.oz.atm.util y dentro creas una clase llamada AbstractTest con el siguiente:

package com.oz.atm.util;

import junit.framework.TestCase;

public abstract class AbstractTest extends TestCase{

}

Esta clase será la super clase para todos nuestros tests y fijense como estamos utilizando clases Abstractas😉.

Ahora vamos a crear una clase de prueba en el mismo paquete llamada LoggerTest, que va a heredar de la clase AbstractTest:

package com.oz.atm.util;

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LoggerTest extends AbstractTest {

    private static final Logger logger= LoggerFactory.getLogger(LoggerTest.class);

    @Test
    public void testLogger(){

        logger.info("----------- INFO");
        logger.debug("----------- DEBUG");
        logger.error("----------- ERROR");

    }
}

La variable looger es la forma en la cual debemos declarar el log para cada clase que lo necesite, la inicializamos llamando al método LoggerFactory.getLogger()  pasando como argumento el nombre de nuestra clase LoggerTest.class.

Ahora la anotación @Test, se utiliza para indicarle a JUnit cual es el método a probar, una clase puede contener uno o más métodos de prueba.

Y dentro de la clase mostramos tres mensajes en diferentes niveles:  info, debug y error.

Para ejecutar el Test lo seleccionamos con el boton derecho del mouse y luego en Test File (CTRl + F6), al final nos mostrará algo así:

12:49:11.333 | INFO  | com.oz.atm.util.LoggerTest - ----------- INFO 
12:49:11.339 | ERROR | com.oz.atm.util.LoggerTest - ----------- ERROR

En la configuración de logback (logback.xml) podemos modificar el nivel para que se muestren los mensajes en debug. Bien ya podemos realizar pruebas unitarias.

3. Hibernate

Para manejar persistencia utilizaremos hibernate, el cual nos proporciona un mecanismo seguro para el acceso a la base de datos.

Vamos a crear el archivo hibernate.cfg.xml en la carpeta src/main/resources con lo siguiente:

<!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <session-factory >
        <property name="useUnicode">true</property>
        <property name="characterSetResults">UTF8</property>
        <property name="characterEncoding">UTF8</property>
        <!--<property name="hibernate.query.substitutions">true 1, false 0</property> -->
        <property name="hibernate.show_sql">true</property>
        <property name="hibernate.format_sql">false</property>
        <property name="hibernate.use_sql_comments">false</property>
        <property name="hibernate.hbm2ddl.keywords">auto-quote</property>
        <property name="hibernate.bytecode.use_reflection_optimizer">true</property>
        <!--<property name="hibernate.transaction.auto_close_session">true</property>-->
        <property name="hibernate.connection.autocommit">false</property>
        <!-- <property name="hibernate.hbm2ddl.auto">update</property>-->
        <property name="hibernate.connection.useUnicode">true</property>
        <property name="hibernate.connection.characterEncoding">UTF8</property>
        <property name="hibernate.connection.charSet">UTF8</property>
        <property name="hibernate.connection.characterSetResults">UTF8</property>

        <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
        <property name="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</property>

        <!-- Las transacciones las va a manejar Spring -->
        <!--<property name="hibernate.transaction.manager_lookup_class">${hibernate.transaction.manager_lookup_class}</property>-->

    </session-factory>
</hibernate-configuration>

4. Spring

Lo utlizaremos para la IoC y la DI, permite administrar el ciclo de vida de los beans y es un excelente contenedor que da soporte a aplicaciones empresariales.

Vamos a crear el archivo applicationContext.xml en la carpeta src/main/resources con lo siguiente:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:util="http://www.springframework.org/schema/util"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:jee="http://www.springframework.org/schema/jee"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd">

    <!-- Spring Hibernate config -->

    <bean id="transactionManager"
          class="org.springframework.orm.hibernate3.HibernateTransactionManager"
          p:sessionFactory-ref="sessionFactory"/>

    <!-- Adm de transacciones-->
    <tx:annotation-driven transaction-manager="transactionManager"/>

    <bean id="sessionFactory"
          class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"
          p:configurationClass="org.hibernate.cfg.AnnotationConfiguration"
          p:configLocation="classpath:hibernate.cfg.xml"
          p:dataSource-ref="ds.cp.c3p0"/>

    <!-- C3p0 DS  work with HSQLDB-->
    <bean id="ds.cp.c3p0"
          lazy-init="true"
          class="com.mchange.v2.c3p0.ComboPooledDataSource"
          destroy-method="close"
          p:driverClass="com.mysql.jdbc.Driver"
          p:user="root"
          p:password="root"
          p:jdbcUrl="jdbc:mysql://localhost:3306/OZ_TEST"
          p:acquireIncrement="5"
          p:idleConnectionTestPeriod="60"
          p:maxPoolSize="100"
          p:minPoolSize="10"
          p:maxStatements="50"/>

    <!-- Apache DS -->
    <bean id="ds.cp.dbcp"
          lazy-init="true"
          class="org.apache.commons.dbcp.BasicDataSource"
          p:driverClassName="com.mysql.jdbc.Driver"
          p:username="root"
          p:password="root"
          p:url="jdbc:mysql://localhost:3306/OZ_TEST"
          p:initialSize="5"
          p:maxActive="5"
          p:maxIdle="5"
          p:defaultAutoCommit="false"
          p:defaultTransactionIsolation="2"
          p:validationQuery="select 1"
          p:testOnBorrow="true"/>

</beans>

Spring tiene un amplio catálogo de clases que brindan apoyo a otros frameworks, como Hibernate y JUnit. Vamos a darle soporte a nuestros test con el applicationContext y vamos a probar la configuración de Hibernate.

Primero vamos a modificar la clase AbstractTest con lo siguiente:

package com.oz.atm.util;

import junit.framework.TestCase;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;

@ContextConfiguration(locations = { "classpath:applicationContext.xml"})
@TestExecutionListeners({TransactionalTestExecutionListener.class})
@RunWith(SpringJUnit4ClassRunner.class)
public abstract class AbstractTest extends AbstractJUnit4SpringContextTests{

}
  • @ContextConfiguration, le dice a nuestros test donde se encuentra el Aplication Context de Sprring.
  • @TestExecutionListeners brinda soporte para que las pruebas de base de datos utilicen transacciones.
  • @RunWith, indica la super clase porporcionada por Spring que se acopla con JUnit para realizar la integración.

¡Listo! ahora todos nuestros test tienen acceso a Spring y soporte para transaccionalidad. Vamos a probarlo, en la clase LoggerTest generamos un nuevo método de prueba:

    @Test
    @Transactional
    public void testConfig(){

        SessionFactory sf= (SessionFactory) applicationContext.getBean("sessionFactory");

        List result = sf.getCurrentSession().createSQLQuery("show tables").list();

        logger.info("Resultado:{}", result);

    }

Si ejecutamos la prueba en la consola mostrará lo siguiente:

17:09:54.767 | INFO  | com.oz.atm.util.LoggerTest - ----------- INFO 
17:09:54.768 | ERROR | com.oz.atm.util.LoggerTest - ----------- ERROR 
17:09:54.832 | INFO  | o.s.t.c.t.TransactionalTestExecutionListener - Began transaction (1): transaction manager [org.springframework.orm.hibernate3.HibernateTransactionManager@76136c55]; rollback [true] 
Hibernate: show tables
17:09:54.917 | INFO  | com.oz.atm.util.LoggerTest - Resultado:[ACCOUNT, CUSTOMER]

Como se puede ver, hibernate ejecuta nuestro query y como resultado nos devuevle las tablas que tenemos en la base.

Con esto ya tenemos la configuración básica del proyecto.

5. Crear la interface gráfica

Como vamos a utilizar Swing lo mejor es hacerlo con NetBeans, así que vamos a crear un nuevo paquete llamado com.oz.atm.gui, y dentro vamos a generar la pantalla, seleccionamos File > New File y en la ventana de selección tomamos Swing GUi Forms y luego JFrame Form.

Y en el nombre escribimos CajeroSwing y damos clic en Finish.

Ahora vamos arrastrar los componentes de la paleta a nuestra ventana comenzando por un JPanel y dentro del panel vamos a agregar dos Buttons, dos FormattedTextField y tres Labels, ahora vamos a ordenarlos de la siguiente forma:

¿Quedó identica verdad :D?

6. Lógica del Negocio

Primero vamos a generar las entidades que representan las tablas de la base para hibernate, en el paquete com.oz.atm.model.persistence  crearmos las siguientes clases con sus metodos get y set:

Customer.java

package com.oz.atm.model.persistence;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Collection;

@Entity
@Table(name = "CUSTOMER")
@org.hibernate.annotations.Entity(dynamicInsert = true, dynamicUpdate = true)
public class Customer implements Serializable{

    private Integer idCustomer;
    private String name;

    @Id
    @Column(name = "id_customer")
    @GeneratedValue(strategy = GenerationType.AUTO)
    public Integer getIdCustomer() {
        return idCustomer;
    }

    public void setIdCustomer(Integer idCustomer) {
        this.idCustomer = idCustomer;
    }

    @Column(name = "name")
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    private Collection<Account> accounts;

    @OneToMany(mappedBy = "customer", fetch= FetchType.LAZY)
    public Collection<Account> getAccounts() {
        return accounts;
    }

    public void setAccounts(Collection<Account> accounts) {
        this.accounts = accounts;
    }
}

Account.java

package com.oz.atm.model.persistence;

import javax.persistence.*;
import java.io.Serializable;

@Entity
@Table(name = "ACCOUNT")
@org.hibernate.annotations.Entity(dynamicInsert = true, dynamicUpdate = true)
public class Account implements Serializable{

    private Integer idAccount;
    private Double balance;

    @Id
    @Column(name = "id_account")
    @GeneratedValue(strategy = GenerationType.AUTO)
    public Integer getIdAccount() {
        return idAccount;
    }

    public void setIdAccount(Integer idAccount) {
        this.idAccount = idAccount;
    }

    @Column(name = "balance")
    public Double getBalance() {
        return balance;
    }

    public void setBalance(Double balance) {
        this.balance = balance;
    }

    private Customer customer;

    @ManyToOne(fetch= FetchType.LAZY)
    @JoinColumn(name = "id_customer", nullable = false)
    public Customer getCustomer() {
        return customer;
    }

    public void setCustomer(Customer customer) {
        this.customer = customer;
    }
}

Agregamos nuestras entidades en el archivo de configuración de hibernate, hibernate.cfg.xml:

<session-factory>
  <!-- Omitted previous lines -->
  <mappingclass="com.oz.atm.model.persistence.Customer"/>
  <mappingclass="com.oz.atm.model.persistence.Account"/>

</session-factory>

Vamos transcribir nuestro diseño UML en clases:

Como estamos trabajando de acuerdo al patrón MVC, comenzaremos por la capa del Modelo, especificamente con el DAO (Data Access Object) , en esta capa se encuentran todas las clases que tienen acceso a los datos de la base, como son Retiros y Transferencias.

Ahora dentro del paquete com.oz.atm.model.dao vas crear a las siguientes clases comenzando por las interfaces y luego las implementaciones:

Retiros.java

package com.oz.atm.model.dao;

public interface Retiros {

    void retirar(Integer idCustomer, double amount);
}

Transferencias.java

package com.oz.atm.model.dao;

public interface Transferencias {

    void transferir(Integer idCustomer, double amount);
}

RetirosImpl.java

package com.oz.atm.model.dao;

import com.oz.atm.model.persistence.Account;
import com.oz.atm.model.persistence.Customer;

public class RetirosImpl extends AbstractDao implements Retiros{

    @Override
    public void retirar(Integer idCustomer, double amount) {

        Customer c = new Customer();
        c.setIdCustomer(idCustomer);

        Account a = new Account();
        a.setCustomer(c);
        a.setBalance(amount);

        sf.getCurrentSession().save(a);

    }
}

TransferenciasImpl.java

package com.oz.atm.model.dao;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TransferenciasImpl extends AbstractDao implements Transferencias {

    public static final Logger LOG = LoggerFactory.getLogger(TransferenciasImpl.class);

    @Override
    public void transferir(Integer idCustomer, double amount) {

        LOG.info("No Cliente:{} , Transferir:{}",idCustomer,amount);

    }
}

AbstractDao.java

Aunque esta clase no esta en nuestro diseño la utilizo para compartir el sessionFactory de hibernate con todos los DAO, es para reutilizar el código.

package com.oz.atm.model.dao;

import org.hibernate.SessionFactory;

public class AbstractDao {

    protected SessionFactory sf;

    public void setSf(SessionFactory sf) {
        this.sf = sf;
    }
}

Ahora configuramos los beans en spring dentro del applicationContext.xml, agregas las siguientes lineas al archivo:

    <bean name="absDao" class="com.oz.atm.model.dao.AbstractDao" p:sf-ref="sessionFactory"/>
    <bean parent="absDao" name="retirosDao" class="com.oz.atm.model.dao.RetirosImpl" />
    <bean parent="absDao" name="trasferenciasDao" class="com.oz.atm.model.dao.TransferenciasImpl" />

Bien, vamos a probar los procesos de forma separada, para ello vamos a crear un Test Unitario para cada uno. Dentro de Test Packages,  creamos el paquete con el nombre com.oz.atm.model.dao y dentro creas las siguientes clases:

RetirosImplTest.java

package com.oz.atm.model.dao;

import com.oz.atm.util.AbstractTest;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

public class RetirosImplTest extends AbstractTest {

    private static final Logger LOG = LoggerFactory.getLogger(RetirosImplTest.class);

    @Resource(name="retirosDao")
    private Retiros retiros;

    @Test
    @Transactional
    public void testRetirar() {

        LOG.info("Prueba de Retiro... ");
        retiros.retirar(1, 100);

    }

}

TransferenciasImplTest.java

package com.oz.atm.model.dao;

import com.oz.atm.util.AbstractTest;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Resource;

public class TransferenciasImplTest extends AbstractTest {

    public static final Logger LOG = LoggerFactory.getLogger(TransferenciasImplTest.class);

    @Resource(name="trasferenciasDao")
    private Transferencias transferencias;

    @Test
    public void testTransferir() throws Exception {

        LOG.info("Prueba de Transferencia...");
        transferencias.transferir(1,2000);

    }
}

Para ejecutarlos seleccionas cada uno con el boton derecho del mouse y tomas la opción Test File.

En el log de Retiros veras algo similar a esto:

12:19:06.949 | INFO  | o.s.t.c.t.TransactionalTestExecutionListener - Began transaction (1): transaction manager [org.springframework.orm.hibernate3.HibernateTransactionManager@15b393a]; rollback [true] 
12:19:06.951 | INFO  | com.oz.atm.model.dao.RetirosImplTest - Prueba de Retiro...
Hibernate: 
    select
        customer_.id_customer,
        customer_.name as name0_ 
    from
        CUSTOMER customer_ 
    where
        customer_.id_customer=?
Hibernate: 
    insert 
    into
        ACCOUNT
        (balance, id_customer) 
    values
        (?, ?)
12:19:07.202 | INFO  | o.s.t.c.t.TransactionalTestExecutionListener - Rolled back transaction after test execution for test context [[TestContext@2c7039 testClass = RetirosImplTest, testInstance = com.oz.atm.model.dao.RetirosImplTest@17becd8, testMethod = testRetirar@RetirosImplTest, testException = [null], mergedContextConfiguration = [MergedContextConfiguration@fa983e testClass = RetirosImplTest, locations = '{classpath:applicationContext.xml}', classes = '{}', activeProfiles = '{}', contextLoader = 'org.springframework.test.context.support.DelegatingSmartContextLoader']]]

Nuestro código ya esta funcionando pero ¡cuidado! en la base de datos no vas a ver el insert ya que los Test’s realizan un rollback de la transacción. Esto lo hace en autómatico spring para mantener el estado de la base y solo lo realiza durante las prubeas, pero cuando lo ejecutemos desde el GUI va a ser una garantía de que el código esta funcionando adecuadamente.

Y en transeferencias verás esto:

13:06:10.187 | INFO  | o.s.o.h.HibernateTransactionManager - Using DataSource [com.mchange.v2.c3p0.ComboPooledDataSource [ acquireIncrement -> 5, acquireRetryAttempts -> 30, acquireRetryDelay -> 1000, autoCommitOnClose -> false, automaticTestTable -> null, breakAfterAcquireFailure -> false, checkoutTimeout -> 0, connectionCustomizerClassName -> null, connectionTesterClassName -> com.mchange.v2.c3p0.impl.DefaultConnectionTester, dataSourceName -> 1b611n58ts2b6vayfqopv|a7cbfd, debugUnreturnedConnectionStackTraces -> false, description -> null, driverClass -> com.mysql.jdbc.Driver, factoryClassLocation -> null, forceIgnoreUnresolvedTransactions -> false, identityToken -> 1b611n58ts2b6vayfqopv|a7cbfd, idleConnectionTestPeriod -> 60, initialPoolSize -> 3, jdbcUrl -> jdbc:mysql://10.13.8.65:3306/OZ_TEST, maxAdministrativeTaskTime -> 0, maxConnectionAge -> 0, maxIdleTime -> 0, maxIdleTimeExcessConnections -> 0, maxPoolSize -> 100, maxStatements -> 50, maxStatementsPerConnection -> 0, minPoolSize -> 10, numHelperThreads -> 3, numThreadsAwaitingCheckoutDefaultUser -> 0, preferredTestQuery -> null, properties -> {user=******, password=******}, propertyCycle -> 0, testConnectionOnCheckin -> false, testConnectionOnCheckout -> false, unreturnedConnectionTimeout -> 0, usesTraditionalReflectiveProxies -> false ]] of Hibernate SessionFactory for HibernateTransactionManager 
13:06:10.270 | INFO  | c.o.a.m.dao.TransferenciasImplTest - Prueba de Transferencia... 
13:06:10.270 | INFO  | c.o.atm.model.dao.TransferenciasImpl - No Cliente:1 , Transferir:2000.0

Para las transferencias estamos imprimiendo un mensaje con los datos, esto es para simular que se esta enviando el dinero a otro lugar.

Hasta aquí ÚNICAMENTE estamos probando los procesos por separado y NO el cajero como tal,  esto es bueno ya que por tener nuestros módulos independientes podemos asegurar que están funcionando sin la necesidad de tener un cajero, es decir que las partes de nuestro cajero estan bien hechas y que van a funcionar correctamente de forma individual.

Vamos a ver el diagrama de clases de lo que hemos creado:

Las clases RetiroImplTest y TransferenciasImplTest, se encargan de probar nuestros procesos por medio de las interfaces de Retiros y Transferencias, cada una tiene una implementación RetirosImpl y TransferenciasImpl, ambas clases heredan de AbstractDao para compartir el sessionFactory de hibernate.

Ahora el Cajero y su implementación, creas las clases del paquete com.oz.atm.service:

Cajero.java

package com.oz.atm.service;

public interface Cajero {

    /**
     * Retiro de efectivo por la cantidad solicitada
     * @param idCustomer identificador del cliente
     * @param amount importe a retirar
     */
    void retirar(Integer idCustomer, double amount);

    /**
     * Transfiere el efectivo a otro Banco
     * @param idCustomer identificador del cliente
     * @param amount importe a transferir
     */
    void transferir(Integer idCustomer, double amount);

}

CajeroBancario.java

package com.oz.atm.service;

import com.oz.atm.model.dao.Retiros;
import com.oz.atm.model.dao.Transferencias;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.annotation.Transactional;

public class CajeroBancario implements Cajero {

    public static final Logger LOG = LoggerFactory.getLogger(CajeroBancario.class);

    private Retiros retiros;
    private Transferencias transferencias;

    public void setRetiros(Retiros retiros) {
        this.retiros = retiros;
    }

    public void setTransferencias(Transferencias transferencias) {
        this.transferencias = transferencias;
    }

    @Override
    @Transactional
    public void retirar(Integer idCustomer, double amount) {
        retiros.retirar(idCustomer,amount);
    }

    @Override
    @Transactional
    public void transferir(Integer idCustomer, double amount) {

        retiros.retirar(idCustomer,amount);
        transferencias.transferir(idCustomer,amount);
    }
}

Aquí solo quiero resaltar que el método transferir ejecuta los dos procesos y en la vida real así es… para transferir dinero primero lo retira de tu cuenta y luego lo envía a otro banco. Por eso el servicio transferir realiza ambas tareas.

Ahora agregamos el bean en la configuración de Spring en el applicationContext.xml:

    <bean id="cajeroBancario"
          class="com.oz.atm.service.CajeroBancario"
          p:retiros-ref="retirosDao"
          p:transferencias-ref="trasferenciasDao"/>

Vamos a probar el cajero creando su Test Unitario dentro de Test Packages:

CajeroBancarioTest.java

package com.oz.atm.service;

import com.oz.atm.util.AbstractTest;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

public class CajeroBancarioTest extends AbstractTest {

    public static final Logger LOG = LoggerFactory.getLogger(CajeroBancarioTest.class);

    @Resource(name="cajeroBancario")
    private Cajero cajero;

    @Test
    @Rollback(true)
    @Transactional
    public void testRetirar() throws Exception {
        cajero.retirar(1, 3000);
    }

    @Test
    @Rollback(true)
    @Transactional
    public void testTransferir() throws Exception {
        cajero.transferir(1, 400);
    }
}

El resultado que vas a ver es la ejecución de ambos Test:

16:20:52.511 | INFO  | o.s.o.h.HibernateTransactionManager - Using DataSource [com.mchange.v2.c3p0.ComboPooledDataSource [ acquireIncrement -> 5, acquireRetryAttempts -> 30, acquireRetryDelay -> 1000, autoCommitOnClose -> false, automaticTestTable -> null, breakAfterAcquireFailure -> false, checkoutTimeout -> 0, connectionCustomizerClassName -> null, connectionTesterClassName -> com.mchange.v2.c3p0.impl.DefaultConnectionTester, dataSourceName -> 1b611n58ts99ljk190scjt|e0d675, debugUnreturnedConnectionStackTraces -> false, description -> null, driverClass -> com.mysql.jdbc.Driver, factoryClassLocation -> null, forceIgnoreUnresolvedTransactions -> false, identityToken -> 1b611n58ts99ljk190scjt|e0d675, idleConnectionTestPeriod -> 60, initialPoolSize -> 3, jdbcUrl -> jdbc:mysql://10.13.8.65:3306/OZ_TEST, maxAdministrativeTaskTime -> 0, maxConnectionAge -> 0, maxIdleTime -> 0, maxIdleTimeExcessConnections -> 0, maxPoolSize -> 100, maxStatements -> 50, maxStatementsPerConnection -> 0, minPoolSize -> 10, numHelperThreads -> 3, numThreadsAwaitingCheckoutDefaultUser -> 0, preferredTestQuery -> null, properties -> {user=******, password=******}, propertyCycle -> 0, testConnectionOnCheckin -> false, testConnectionOnCheckout -> false, unreturnedConnectionTimeout -> 0, usesTraditionalReflectiveProxies -> false ]] of Hibernate SessionFactory for HibernateTransactionManager 
16:20:52.672 | INFO  | o.s.t.c.t.TransactionalTestExecutionListener - Began transaction (1): transaction manager [org.springframework.orm.hibernate3.HibernateTransactionManager@ff6b0d]; rollback [true] 
16:20:52.686 | INFO  | com.oz.atm.model.dao.RetirosImpl - Cliente:1 ,Retirar:400.0 
Hibernate: 
    insert 
    into
        ACCOUNT
        (balance, id_customer) 
    values
        (?, ?)
16:20:52.791 | INFO  | c.o.atm.model.dao.TransferenciasImpl - No Cliente:1 , Transferir:400.0 
16:20:52.804 | INFO  | o.s.t.c.t.TransactionalTestExecutionListener - Rolled back transaction after test execution for test context [[TestContext@17faec2 testClass = CajeroBancarioTest, testInstance = com.oz.atm.service.CajeroBancarioTest@1aa476d, testMethod = testTransferir@CajeroBancarioTest, testException = [null], mergedContextConfiguration = [MergedContextConfiguration@375452 testClass = CajeroBancarioTest, locations = '{classpath:applicationContext.xml}', classes = '{}', activeProfiles = '{}', contextLoader = 'org.springframework.test.context.support.DelegatingSmartContextLoader']]] 
16:20:52.816 | INFO  | o.s.t.c.t.TransactionalTestExecutionListener - Began transaction (2): transaction manager [org.springframework.orm.hibernate3.HibernateTransactionManager@ff6b0d]; rollback [true] 
16:20:52.817 | INFO  | com.oz.atm.model.dao.RetirosImpl - Cliente:1 ,Retirar:3000.0 
Hibernate: 
    insert 
    into
        ACCOUNT
        (balance, id_customer) 
    values
        (?, ?)
16:20:53.033 | INFO  | o.s.t.c.t.TransactionalTestExecutionListener - Rolled back transaction after test execution for test context [[TestContext@17faec2 testClass = CajeroBancarioTest, testInstance = com.oz.atm.service.CajeroBancarioTest@1d7b098, testMethod = testRetirar@CajeroBancarioTest, testException = [null], mergedContextConfiguration = [MergedContextConfiguration@375452 testClass = CajeroBancarioTest, locations = '{classpath:applicationContext.xml}', classes = '{}', activeProfiles = '{}', contextLoader = 'org.springframework.test.context.support.DelegatingSmartContextLoader']]] 

Nuestro código ya esta completo, pero antes enlazarlo a la interface gráfica vamos a platicar de las transacciones con Spring y Hibernate.

Transacciones

Una regla primordial en Hibernate es el uso de transacciones para poder ejecutar operaciones a la base de datos, cualquier consulta debe utilizar una transacción, TODAS incluso las de solo lectura. Si ejecutas código fuera de una transacción obtendras una excepción.

Pero ¿que pasa con nuestro código? funciona bien…  ¿donde estan esas transacciones ?, fácil Spring se encarga de administrarlas sin que nos demos cuenta… bendito Spring  hace el trabajo por tí, se encarga de obtener la sesión, abrir la transacción y cerrarla y gestiona las excepciones que puedan ser lanzadas, sin embargo nosotros debemos indicarle donde se van a utilizar.

Para esto utilizamos la anotación @Transactional, el siguiente diagrama muestra la ubiación de las transacciones de nuestro ejemplo.

Pero para seguir platicando de esto vamos a ver para que sirven…

Por ejemplo, cuando tú utilizas un cajero generalmente los procesos son unicos y controlados, es decir no puedes sacar dinero de dos cajeros al mismo tiempo con tu misma cuenta, o si haces una transferencia a otro banco se valida que haya fondos en tu cuenta antes de realizar la operación.

Pero que pasaría si en ese momento que quieres transferir dinero y no hay comuniación con el otro banco ¿que le sucede a tu dinero?

Fácil, como cliente lo que esperas es que si tu dinero no puede viajar en ese momento te notifiquen y tu cuenta siga teniendo el mismo saldo, esto es algo que NO percibimos al principio del desarrollo pero es una funcionalidad natural que el usuario o nosotros mismos esperamos que haga… o a caso ¿vamos a crear cajeros que se roben el dinero?… afortunadamente las transacciones se encargan de este tema y nuestro código ya esta preparado para esto y no debemos hacer nada adicional.

Las transacciones de base de datos operan de la siguiente manera:

  • Se abre el medio de comunicación con la base.
  • Se ejecutan una o varias consultas y modificaciones mediante querys.
  • Si todas las consultas fueron realizadas satisfactoriamente durante la transaccion, se aplican los cambios en la base, es decir se ejecuta un COMMIT y se cierra la comunicación (la transacción).
  • Si al ejecutar los querys hay errores, todas las modificaciones realizadas en la base hechas hasta ese momento son revertidas a su estado inicial, es decir se ejecuta un Rollback y se cierra la transacción.

En pocas palabras se puede ver como un mecanismo para garantizar que los datos van ser integros y consistentes en el momento de persistirlos (almacenarlos) en la base.

Veamos el método testRetirar de la clase RetirosImplTest:

    @Test
    @Transactional
    public void testRetirar() {

        LOG.info("Prueba de Retiro... ");
        retiros.retirar(1, 100);

    }

La anotación @Transactional le dice a Spring que ese método debe ser encapsulado con una transacción, la cual va a estar vigente durante toda la ejecución del mismo, si llega a ocurrir un error dentro del método la transacción va a revertir todos los cambios que se hayan hecho en la base de datos.

Si ejecutas el Test verás lo siguiente en el log:

12:19:06.949 | INFO  | o.s.t.c.t.TransactionalTestExecutionListener - Began transaction (1): transaction manager [org.springframework.orm.hibernate3.HibernateTransactionManager@15b393a]; rollback [true] 
12:19:06.951 | INFO  | com.oz.atm.model.dao.RetirosImplTest - Prueba de Retiro...
12:19:06.953 | INFO  | com.oz.atm.model.dao.RetirosImpl - Cliente:1 ,Retirar:100.0 
Hibernate: 
    insert 
    into
        ACCOUNT
        (balance, id_customer) 
    values
        (?, ?)
12:19:07.202 | INFO  | o.s.t.c.t.TransactionalTestExecutionListener - Rolled back transaction after test execution for test context [[TestContext@2c7039 testClass = RetirosImplTest, testInstance = com.oz.atm.model.dao.RetirosImplTest@17becd8, testMethod = testRetirar@RetirosImplTest, testException = [null], mergedContextConfiguration = [MergedContextConfiguration@fa983e testClass = RetirosImplTest, locations = '{classpath:applicationContext.xml}', classes = '{}', activeProfiles = '{}', contextLoader = 'org.springframework.test.context.support.DelegatingSmartContextLoader']]]

Se abre la transacción y al final se ejecuta un rollback, esto significa que todo el proceso fue correcto, sin embargo no se aplicaron los cambios en la base de datos.

¿ Y por que no se aplicaron? porque Spring en los Test unitarios SIEMPRE aplica un Rollback por defecto.

Vamos a decirle que no queremos el rollback agregando lo siguiente:

    @Test
    @Transactional
    @Rollback(false)
    public void testRetirar() {

        LOG.info("Prueba de Retiro... ");
        retiros.retirar(1, 100);

    }

Al ejecutarlo el log mostrará esto:

17:01:03.539 | INFO  | o.s.t.c.t.TransactionalTestExecutionListener - Began transaction (1): transaction manager [org.springframework.orm.hibernate3.HibernateTransactionManager@d4446b]; rollback [false] 
17:01:03.540 | INFO  | com.oz.atm.model.dao.RetirosImplTest - Prueba de Retiro...  
17:01:03.540 | INFO  | com.oz.atm.model.dao.RetirosImpl - Cliente:1 ,Retirar:100.0 
Hibernate: 
    insert 
    into
        ACCOUNT
        (balance, id_customer) 
    values
        (?, ?)
17:01:03.615 | INFO  | o.s.t.c.t.TransactionalTestExecutionListener - Committed transaction after test execution for test context [[TestContext@429b5f testClass = RetirosImplTest, testInstance = com.oz.atm.model.dao.RetirosImplTest@e690ac, testMethod = testRetirar@RetirosImplTest, testException = [null], mergedContextConfiguration = [MergedContextConfiguration@1243198 testClass = RetirosImplTest, locations = '{classpath:applicationContext.xml}', classes = '{}', activeProfiles = '{}', contextLoader = 'org.springframework.test.context.support.DelegatingSmartContextLoader']]]

Con esto ya se deben de ver los cambios aplicados en la base.

Ahora la pregunta interesante a todo este tema es ¿En que parte debo de utilizar las transacciones? y la respuesta es:

En los procesos que ejecutan una o más tareas y que deben ser aplicadas en un solo conjunto, es decir en un solo paso, además  los Test’s que realizan consultas y modificaciones a base de datos también deben declararlo

Vamos a ver nuestro diagrama donde estan las transacciones, la clase RetirosImpl es utilizado por RetirosImplTest y CajeroBancario, estas clases son las responsables que deben declarar las transacciones, porque son los que deciden si hacen o no los cambios a la base de datos, la estructura es sencilla el DAO otroga el acceso a los datos pero la clase que implementa administra el proceso.

Esto significa que RetirosImpl delega la responsabilidad de ejecutar el commit o el rollback al componente que lo está utilizando, así el proceso retirtar puede ser combinado con otros procesos.

Hay que tener cuidado para saber donde utilizamos las transacciones, en mi opinion y por lo que he visto TODOS los DAO’s NO deben incluir el manejo de transacciones, por diseño esto simplifica muchisimo la capa de acceso a datos y asegura que todas las consultas sean reutilizables por cualquier otro componente.

Vamos a clarificar esto con un ejemplo, imagina que declaramos la transacción en la clase RetirosImpl, al hacer esto nuestro cajero se puede robar el dinero.

Cuando quieras retirar dinero todo va a funcionar bien, pero al transferir primero se va a retirar de la cuenta y se ejecuta el COMMIT en ese momento, después lo va a enviar, pero si falla el envió ¿que le pasa a tu dinero?… ¿ya ves cúal es el problema ? tendrías que desarrollar algo extra para regresar ese dinero a la cuenta por que la transacción esta mal ubicada.

Bueno hablemos de los Test’s, si ejecutas la clase CajeroBancarioTest prueba el funcionamiento de la clase CajeroBancario, pero ambas clases tienen declarada la anotación @Transactional ¿cúal es la que se va o tomar? ¿la del test o la del servicio?, la respuesta es la primera en ser ejecutada.

Internamente Spring detecta que hay una transacción anidada pero su configuración por defecto respeta la transacción externa, en este caso la del Test (este comportamiento puede modificarse con el atributo Propagation dentro de la anotación).

Entonces si el Cajero es utilizado desde el Test va a ejecutar la transacción del Test y si es utilizado por el flujo normal del programa o cualquier otro componente va a ejecutar la que esta dentro de la clase CajeroBancario.

7. Conectar los servicios con la interface gráfica

Ya tenemos todo construido y probado,  lo que nos falta es conectar nuestro servicio del cajero con la interface swing:

Abres la clase CajeroSwing y declaras el atributo  Cajero con su método setter:

publicclass CajeroSwing extends javax.swing.JFrame {

    private Cajero cajero;

    publicvoid setCajero(Cajero cajero) {
        this.cajero = cajero;
    }

/** rest of code **/

En la vista de diseño vas seleccionar el boton “Retiro” y con el menú contextual seleccionas Events > Action > actionPerformed y en la porción de código que te genera escribes lo siguiente:

privatevoid jButton1ActionPerformed(java.awt.event.ActionEvent evt) {        

        lresult.setText("");

        Double amount=Double.parseDouble(famount.getText()) ;
        cajero.retirar( Integer.parseInt(fclient.getText()),amount );
        lresult.setText("Withdraw successfully for : "+ amount);
    }

Ahora repites lo mismo para el boton “Transferencia” y colocas el siguiente código:

privatevoid jButton2ActionPerformed(java.awt.event.ActionEvent evt) {        

        lresult.setText("");

        Double amount=Double.parseDouble(famount.getText()) ;
        cajero.transferir(Integer.parseInt(fclient.getText()),amount );
        lresult.setText("Transfer successfully  for : "+ amount);
    }

Nota: los nombres de tus variables pueden ser diferentes a las mias.

Editas el applicationContext.xml agregas el siguiente bean:

<beanid="cajeroSwing"class="com.oz.atm.gui.CajeroSwing" p:cajero-ref="cajeroBancario"/>

Creas una nueva clase llamada Main en el paquete com.oz.atm con el siguiente contenido:

package com.oz.atm;

import com.oz.atm.gui.CajeroSwing;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

publicclass Main {

    publicstaticvoid main(String[] args) {

        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

        final CajeroSwing cajero = (CajeroSwing) context.getBean("cajeroSwing");

        cajero.setVisible(true);
    }

}

Si ejecutamos la clase Main ya podemos utilizar nuestro Cajero:

La idea de este ejemplo es dar un contexto de análisis y diseño, así como su impacto en el desarrollo. El código es realmente muy sencillo.

Aquí dejo la url al repositorio en github con el código del proyecto:

Espero que sea de ayuda.

Saludos!

4 comentarios en “OZ Ejemplo Cajero ATM

    • jaehoo dijo:

      Con el IntelliJ, me genera automaticamente el UML a partir del código y como este ejemplo ya lo vengo manejando para capacitación fui creando las clases y el uml se hace solo.

      Saludos

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s