martes, 2 de marzo de 2010

Tres Amigos de Persistencia (DAO - DAOFactory - BO)

Hoy en día, aún con todo el buen conocimiento acumulado que hay en patrones de diseño, ingenieros de software todavía tienden (tendía hasta hace un tiempo :)) a programar clases donde la lógica de negocio esta mezclada con la lógica de persistencia. Inclusive los más experimentados desarrolladores omiten este aspecto importante de la arquitectura de software, quizás por falta de conocimiento, o talvez solo por la prisa de sacar una funcionalidad rápidamente. Me parece que la última razón es la más común.

¿Por qué deberíamos molestarnos tanto con esta separación? Bien, la idea no es ahondar mucho en lo que es básico en arquitectura de software: programación en multicapas, sino más bien ayudar con algunos patrones útiles para cumplir con este aspecto de una aplicación bien diseñada. Creo que la mayoría de los desarrolladores están concientes de este principio de separación entre capa de lógica y capa de persistencia, pero lo que tal vez no es tan obvio es cómo se logra codificar las clases para lograr esta independencia.


Primero vamos a comenzar con el patrón más conocido: DAO por sus siglas en inglés de "Data Access Object" u Objeto de Acceso de Datos. Citando de un libro de patrones de diseño [1]:

Problema:

Usted quiere encapsular el acceso y manipulación de datos en una capa separada.

Fuerzas:

  1. Usted desea implementar los mecanismos de acceso a datos para accesar y manipular los datos en el almacenamiento de persistencia.
  2. Usted quiere desacoplar la implementación del almacenamiento de persistencia del resto de su aplicación.
  3. Usted quiere proveer una API uniforme de acceso a datos para un mecanismo de persistencia con varios tipos de fuentes de datos (data sources),tales como: RDBMS, LDAP, OODB, repositorios XML, archivos planos, etc.
  4. Usted quiere organizar la lógica de acceso de datos y encapsular las características propietarias para facilitar el mantenimiento y la portabilidad.

Las clases DAO contiene toda la lógica para conectar por ejemplo una base de datos y obtener todos los datos necesarios correspondientes a las tablas. Un aspecto importante de cualquier clase DAO es que la implementación de cualquier método que retorne datos, nunca debe retornar tipos relacionados con el propietario de almacenamiento. Por ejemplo una implementación DAO de JDBC nunca debe retornar un ResultSet, de lo contrario no se estaría cumpliendo con el punto #4. La aplicación se acoplaría con la implementación de JDBC. Por eso es que en el siguiente diagrama UML el objeto retornado de la clase DAO es un "TransferObject" (Objeto de transferencia). No hay que detallar mucho en este último patrón sino solamente mencionar que tan solo retornando objetos de dominio es suficiente para asegurarse el desacoplo de la capa de negocio con la de persistencia.





El implementar el patrón DAO en nuestra aplicación no es suficente para desacoplar 100% nuestra capa de negocio con la de persistencia. Imaginemos que tenemos la siguiente clase DAO y su cliente que la consume:


package com.foo.dao.jdbc;

class FooDAOJDBCImpl {
public void updateFoo(Foo foo) {
...
}
}

public class FooClient {
void updateChangesInFoo(Foo foo) {
com.foo.dao.jdbc.FooDAOJDBCImpl fooDAOJDBCImpl = new com.foo.dao.jdbc.FooDAOJDBCImpl();
fooDAOJDBCImpl.update(foo);
}
}

Nótese que el cliente necesita instanciar directamente la implementación JDBC. Aún cuando es oculto para el cliente cómo la clase DAO internamente actualiza los datos en el almacenamiento de persistencia, el cliente aún puede conocer que la implementación utiliza JDBC para persistir los datos. Si la implementación de JDBC es reemplazada por otra, el código del cliente tendrá que ser actualizado para usar las nuevas clases DAO.

Para hacer nuestro diseño más flexible a este tipo de cambios, y también para preparar nuestro código para unidades de prueba (uso de clases Mock), podemos unir en matrimonio nuestro patrón DAO con el conocidísimo AbstractFactory para tener un hijo llamado DAOFactory. La clase DAOFactory usa reflexión (en esta versión del patrón) para instanciar las clases DAO.





public interface DAO {

}

public interface FooDAO extends DAO {
public void update(Foo foo);
}

package com.foo.dao.jdbc;
public class FooJDBCImpl implements FooDAO {
public void update(Foo foo) {
....
}
}

public class FooClient {
void updateChangesInFoo(Foo foo) {
FooDAO dao = (FooDAO) DAOFactory.getDAO("foo") ;
dao.update(foo);
}
}



No es hasta el tiempo de ejecución que se conoce cuál clase DAO será usada para ejecutar la función de persistencia. El nombre de las clases se pueden guardar en un archivo de propiedades. Si la implementación de la clase DAO cambia, será totalmente trasparente para la clase cliente.

/**
Properties in some file:
foo=com.foo.dao.jdbc.FooDAOJDBCImpl
foo2=com.foo.dao.jdbc.Foo2DAOJDBCImpl

**/

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Properties;

public class DAOFactory {

private static Properties props = new Properties();
private static boolean loadedProperties = false;
private static String propertiesPath;

public static void init(String propertiesPath) {
propertiesPath = path;
}

public static DAOFactory getDAO(String name) {
try {

Class daoClass = Class.forName( getClass( name ) );
return (DAO) daoClass.newInstance();
}
catch (ClassNotFoundException e) {
e.printStackTrace();
return null;
}
catch (Exception e) {
e.printStackTrace();
return null;
}

}

private static String getClass( String propertyName ) {
String className = null;
try {

if ( !loadedProperties ) {

FileInputStream file = new FileInputStream( propertiesPath );
props.load( file );
loadedProperties = true;
}

className = props.getProperty( propertyName, "");
if ( className.length() == 0)
return null;
}
catch ( FileNotFoundException e) {
e.printStackTrace();
}
catch ( IOException e) {
e.printStackTrace();
}
catch (Exception e) {
e.printStackTrace();
}
return className;
}

}
El último patrón de diseño para completar nuestra banda es el "Business Object" (BO) u Objeto de Negocio. Aún estoy aprendiendo a usarlo correctamente; mientras escribía este artículo me di cuenta que necesito hacer algunos arreglos a una implementación que tengo de este patrón. De todas maneras, uno de los principales propositos de este patrón es separar la lógica de persistencia con la lógica de negocio. Normalmente en nuestas aplicaciones tenemos modelos conceptuales algo complejos conteniendo objetos interrelacionados, estructurados y compuestos. Estas relaciones de objetos compuestos requieren bastante lógica solo para los mecanismos de persistencia. Así que para evitar mezclar estos dos tipo de lógicas, se agrega una capa intermedia
entre la lógica de negocios y la capa de acceso de datos; la capa de objetos de negocio.

Supongamos que tenemos una clase Foo conteniendo una lista de objetos tipo Foo2:

public class Foo {
private List<Foo2> foo2s;
private String someAttribute;

}
public class Foo2 {
private String someAttribute;
}


Si queremos persistir nuestra clase Foo creamos dos clases BO. La clase FooBO in el principal punto de entrada para guardar todos los objetos compuestos:


public class FooBO {

public void saveFoo(Foo foo) {
FooDAO fooDAO = (FooDAO) DAOFactory.getDAO("foo") ;
fooDAO.saveBasicFooInfo(foo);
Foo2BO foo2Bo = new Foo2BO();

for (Foo2 foo2 : foo.getFoo2s()) {
foo2Bo.saveFoo2(foo2);
}
}
}

public class Foo2BO {
public void saveFoo2(Foo foo) {
FooDAO2 fooDAO2 = (FooDAO2) DAOFactory.getDAO("foo2") ;
fooDAO2.saveFoo2(foo2);
}
}

Pueden haber distintas maneras de implementar cualquiera de los tres patrones descritos en este artículo; nada está escrito en piedra. La idea era dar un vistazo rápido en estos tres patrones. Comentarios son bienvenidos.

[1] Deepak Alur, John Crupi, Dan Malks. "Core J2EE Patterns, Best Practices and Design Strategies", 2003. Pags: 462,463.

4 comentarios:

  1. Metiendo la cuchara

    Iniciar, felicitandolo por el articulo.

    Seguido, una critica y una observacion. Iniciemos por la ultima:

    Un objeto de transferencia (DTO) se trata de un objeto que se mappea de forma FISICA con el objeto que se persiste y se trata de un simple bean,
    composicion de ellos o colecciones de los mismos. Dentro de los patrones J2EE, existe el View y el DTO, a continuacion un ejemplo que muestra la diferencia

    Supongamos que tenemos un recurso de datos del siguiente tipo

    Persona nombre : String
    segundoNombre : String
    apellidoUno: String
    apellidoDos: String
    apodo: String
    fechaDeNacimiento: TimeStamp
    cedula: long


    El anterior pseudocodigo muestra como se debe almacenar fisicamente esos datos, pero quiza logicamente sea mas semantico algo como:

    class PersonaView {


    private Long cedula; private String nombre; private Integer edad; private Date fechaDeNacimiento; }

    Y nuestro DTO (el objeto que retornara el DAO), podria mapearse asi

    class PersonaDTO {

    private Long cedula; private String nombre; private String segundoNombre private String apellidoUno; private String apellidoDos; private String apodo;
    TImeStamp fechaDeNacimiento; }

    Ambos objetos son muy similares entre si; la diferencia radica en que el DTO representa la forma en como se almacenaran los datos, no importa el donde
    se guardara, si no como. Por eso decimos que es fisico, pero quiza en las vistas y controlladores, que invoquen a los BO, queremos un objeto algo mas
    logico, semantico y sencillo de usar y mostrar al usuario; un VIew

    Asi pues podriamos tener el siguiente codigo:





    PersonaBO {


    PersonaView findByCedula (Long cedula) {


    PersonaDTO dto = this.personaDAO.getPersona (cedula)

    PersonaView view = this.PersonaConverter.toPersonaView(dto)

    return view; }


    }


    el toPersonaView podria ser algo como:


    toPersonaView (PersonaDTO dto) {

    PersonaView view = ....

    view.setNombre( dto.getNombre() + " " + dto.getSegundoNombre() + " " + dto.getApellidoUno() + " " + dto.getApellidoDos() + ", conocido como: " +
    dto.getApodo()); view.setFechaDeNacimiento(this.toDate(dto.getFechaDeNacimiento()); .... }


    Este enfoque, representa mas trabajo y mantenimiento, pues un nuevo atributo podria implicar cambios en todas las clases, sin embargo permite que los objetos que
    tengan el rol logico, sean mas semanticos, que un simple DTO, ademas que desacopla ciertamente las capas.


    Por ultimo, esta muy bien el asunto del Factory, pero de cara a los marcos actuales de desarrollo, se suele utilizar mas el enfoque de Ioc en lugar de
    un Factory, aunque ya por esto no deja de ser valido el uso de este patron, al final la inversion de control, no es mas que un factory muy bonito.

    ResponderEliminar
  2. No me gusta el uso de reflexión en el factory, cuesta mucho usarlo en tiempo de ejecución

    ResponderEliminar