Buscar en este blog....

miércoles, 20 de abril de 2011

Programación Orientada a Aspectos. ¿Por qué usar POA? (Parte 2)

En la parte 1, di una introducción a lo que es la programación orientada a aspectos de una forma bastante teórica. En esta segunda parte, voy a mostrar un ejemplo práctico muy simplificado para poder asentar un poco más el concepto. Para el ejemplo utilicé el lenguaje Java, y la extensión del lenguaje Java que implementa aspectos AspectJ. Ya he contado como compilar con AspectJ en NetBeans.

Bueno, ¿en qué consiste la POA? Voy a poner un ejemplo muy fácil que ilustra el potencial que nos ofrece la POA Supongamos una aplicación de transacciones bancarias. (Este es el típico ejemplo que se menciona en todos lados, y lo voy a poner en mi versión porque es muy ilustrativo).

Vamos a tener una clase cuyo nombre es Cuenta y es una versión muy simplificada de una cuenta de un banco. Esta clase, tiene un atributo llamado saldo de tipo Double que indica el saldo en la cuenta. Y vamos a tener tres métodos: Uno que llamado hacerDeposito() con un único parámetro de tipo Double que indica la cantidad a depositar en la cuenta. Otro llamado hacerExtraccion() que cuenta con un solo parámetro que indica la cantidad de dinero que se quiere extraer de la cuenta. El método restante se llama hacerTransferencia() y tiene dos parámetros: uno indica la cantidad de dinero y el otro es la cuenta a la que se quiere hacer la transferencia. Además tendremos una típica -y muy simplificada- clase que representa un cliente del supuesto "banco" llamada Cliente. Para seguir simplificando (cosa que se me está haciendo mala costumbre ya) un cliente sólo puede tener una cuenta. Ilustremos con UML:



A continuación, les muestro un ejemplo en java bien simple. Lo que haré en el método Main() será crear dos clientes (Pepe y Juan) y crear una cuenta para cada uno. Después, le asignaré a Pepe una cuenta y a Juan la otra. Finalmente, haré una extracción de la cuenta de Pepe; y -felizmente para Juan- terminaré con una transferencia de dinero de la cuenta de Pepe a la de Juan. Aunque lo que realiza el método Main(), por el momento no nos interesa. Veamos el código de ambas clases (sin los getter y los setters porque son muy triviales).

Veamos la definición de los métodos hacer...() de la clase Cuenta:

public void hacerDeposito(Double cantidad) {
 if(cantidad <= 0) {
     System.out.println("No hay dinero suficiente para hacer el depósito");
 }
 else {         
     Date hora = new Date();         
     SimpleDateFormat formatoDeFecha = new SimpleDateFormat("yyyy.MM.dd G 'at' HH:mm:ss");         
     //Realizamos el depósito
     this.saldo += cantidad;         
     //Registramos el movmiento
     System.out.println("Movimiento realizado a las " + formatoDeFecha.format(hora)) ;         
 }
}

public void hacerTransferencia(Double cantidad, Cuenta cuentaDestino)
{
 if(this.saldo < cantidad) {
     System.out.println("No hay fondos suficientes para la transferencia.");         
 }
 else {
     Date hora = new Date();         
     SimpleDateFormat formatoDeFecha = new SimpleDateFormat("yyyy.MM.dd G 'at' HH:mm:ss");        
     cuentaDestino.hacerDeposito(cantidad);
     this.saldo -= cantidad;
     //Registramos el movmiento
     System.out.println("Movimiento realizado a las " + formatoDeFecha.format(hora)) ;
 }
}

public void hacerExtraccion(Double cantidad) {

 if(this.saldo < cantidad) {
     System.out.println("No hay fondos suficientes para la extracción.");         
 }
 else {         
     Date hora = new Date();         
     SimpleDateFormat formatoDeFecha = new SimpleDateFormat("yyyy.MM.dd G 'at' HH:mm:ss");         
     //Extraemos la cantidad del saldode la cuenta.
     this.saldo -= cantidad;         
     //Registramos el movmiento
     System.out.println("Movimiento realizado a las " + formatoDeFecha.format(hora)) ;
 }
}
Examinemos un poco el código: en todos métodos comenzamos con una verificación de la cantidad que se quiere extraer, depositar o transferir. Esta verificación es necesaria, ya que ciertamente sería un error, por ejemplo, depositar una cantidad negativa de dinero de una cuenta; también sería un error permitir una extracción de más cantidad de dinero de la que se dispone. Estas situaciones deben evitarse si o si.

Por otra parte, vemos que una vez que se comprueban las cantidades y que esta todo bien, se procede a realizar el deposito o la extracción o la transferencia (según sea el caso). Pero para esto, he supuesto (a modo de regla de negocio) que el banco necesita tener un registro (logging) de cada movimiento realizado en cada cuenta. Por eso es que creé una variable llamada hora y la mostré en pantalla. Lo más común no es mostrarlo en pantalla sino almacenarlo en un archivo de logs, pero lo hice así por practicidad.

Lo interesante, curioso, y que tenemos que tener en cuenta de estos métodos, es que los tres repiten el proceso de registrar un movimiento. Y no solo eso, sino que además se hace en los tres métodos de la misma forma! Lo cual es una muy mala práctica. Nunca debería repetirse código, eso lo sabemos bien. Entonces, he aquí un problema. ¿Cómo lo solucionamos?...

Algún iluminado podría indicar hacer lo siguiente: Creamos un cuarto método dentro de la clase al que podríamos llamar registrarMovimiento(). Este método se encargaría sólo de la parte del registro del movimiento; y cada uno de los anteriores tres métodos lo llama cuando ha concluido un movimiento. Genial. Veamos cómo quedaría registrarMovimiento():

private void registrarMovimiento()
{
 Date hora = new Date();         
 SimpleDateFormat formatoDeFecha = new SimpleDateFormat("yyyy.MM.dd G 'at' HH:mm:ss");         
 System.out.println("Movimiento realizado a las " + formatoDeFecha.format(hora)) ;
}

Y ahora, el código de los otros tres métodos se modifica para llamar a registrarMovimiento() quedando así (sólo mostramos dos, pero el tercero seguiría la misma lógica que éstos):

public void hacerTransferencia(Double cantidad, Cuenta cuentaDestino)
{
 if(this.saldo < cantidad) {
     System.out.println("No hay fondos suficientes para la transferencia.");         
 }
 else {
     cuentaDestino.hacerDeposito(cantidad);
     this.saldo -= cantidad;
     //Registramos el movmiento
     registrarMovimiento();
 }
}

public void hacerExtraccion(Double cantidad) {

 if(this.saldo < cantidad) {
     System.out.println("No hay fondos suficientes para la extracción.");         
 }
 else {
     //Extraemos la cantidad del saldode la cuenta.
     this.saldo -= cantidad;         
     //Registramos el movmiento
     registrarMovimiento();         
 }
}
Pero lo malo de esta solución, es que si se efectuara otro movimiento que no fuera "dentro" de la clase Cuenta sino "dentro" de otra clase, esta clase tendría que tener su propio método registrarMovimiento(), y nuevamente estaríamos repitiendo código, pero entre clases. Ante este nuevo problema, podemos proponer otra solución: creamos una clase que se encargue específicamente de registrar movimientos; y cualquier método de cualquier clase puede instanciarla y llamar al método que registra movimientos. Podríamos, por ejemplo, crear una clase llamada RegistroDeMovimientos con un método público llamado registrarMovimiento(). Parece que esta es la mejor solución. Sin embargo no es así. Sigue habiendo "algo malo". ¿Qué es?...

Está claro: aunque tengamos una clase aparte que registre los movimientos, cada clase debe instanciar y llamar al método de esa clase. Es decir, seguimos repitiendo código. Cada método que deba registrar un movimiento, debe repetir el mismo código que los otros métodos que también deban registrar un movimiento. ¡Que feo! ¡Y no solo eso! Además, el hecho de registrar un movimiento -ya sea dentro del mismo método o llamando a RegistroDeMovimientos.registrarMovimiento()- significa introducir código que no pertenece exclusivamente al movimiento. El movimiento sólo debería realizar el movimiento y nada más. El registro debería hacerse aparte, no debería ser responsabilidad de un depósito, o una extracción o una transferencia. Esta forma de registrar un movimiento, ensucia el código que se refiere a los métodos de la clase Cuenta. Es decir, lo ideal sería que estos métodos se vieran así:

public void hacerDeposito(Double cantidad) {
 if(cantidad <= 0) {
     System.out.println("No hay dinero suficiente para hacer el depósito");
 }
 else { 
     this.saldo += cantidad;
 }
}

public void hacerTransferencia(Double cantidad, Cuenta cuentaDestino)
{
 if(this.saldo < cantidad) {
     System.out.println("No hay fondos suficientes para la transferencia.");         
 }
 else {
     cuentaDestino.hacerDeposito(cantidad);
     this.saldo -= cantidad;
 }
}

public void hacerExtraccion(Double cantidad) {

 if(this.saldo < cantidad) {
     System.out.println("No hay fondos suficientes para la extracción.");         
 }
 else {
     this.saldo -= cantidad;
 }
}
Como dije, sería ideal que a pesar de no llamar a registrarMevimiento(), el movimiento se registrara igual. Eso sería ideal, ¿no? No ensuciaríamos código y tampoco lo repetiríamos. La clase sería tremendamente cohesiva. Parece utópico ¿verdad? Bueno, para nuestra felicidad, esto es realizable y es acá donde entran en juego los "aspectos".

En la tercer parte, voy a mostrar cómo implementar un aspecto que solucione el problema planteado.

3 comentarios:

Comments are subject to moderation, only in order to avoid insults and disguising things.

Los comentarios están sujetos a moderación, solo con el fin de evitar insultos y cosas por el estilo.