Dos DAO: un problema que ya conocemos
En el material anterior creamos ProductoDAO para que el servicio no tuviera que escribir consultas SQL. Ahora imaginemos que nuestra tienda comienza a registrar clientes. Necesitamos también ClienteDAO.
Ambos necesitan guardar, listar, modificar y eliminar, pero trabajan con distintos objetos y distintas tablas. ¿Qué parte se repite? ¿Y cuál necesariamente será diferente?
Recuperamos la diferencia entre clase e interfaz
Una clase puede definir atributos, constructores y métodos con comportamiento. Una interfaz permite establecer un contrato: qué operaciones debe ofrecer una clase que decida implementarla.
En nuestro caso el contrato dirá que un DAO permite guardar, listar, modificar y eliminar. No dirá cuál es la tabla, qué SQL utiliza ni cómo abre la conexión. Eso seguirá siendo responsabilidad del DAO concreto.
Una interfaz no es una clase que «hace varias cosas automáticamente». Una clase puede implementar varias interfaces, pero eso no equivale a heredar la implementación completa de varias clases.
Primero, un contrato sin genéricos
Vamos a comenzar con un ejemplo que utiliza solamente los tipos que ya conocemos:
public interface ProductoPersistencia {
void guardar(Producto producto) throws SQLException;
List<Producto> listar() throws SQLException;
void modificar(Producto producto) throws SQLException;
void eliminar(long codigo) throws SQLException;
}Observa que hay cabeceras de métodos terminadas en punto y coma. En este contrato no escribimos los bloques con { } que ejecutan SQL.
throws SQLException comunica que estas operaciones pueden propagar un error JDBC. No significa que la interfaz se conecte ni que gestione por sí sola la excepción.
¿Cómo se compromete ProductoDAO con ese contrato?
La palabra reservada implements conecta una clase con la interfaz que se compromete a implementar.
public class ProductoDAO implements ProductoPersistencia {
@Override
public void guardar(Producto producto) throws SQLException {
// Aquí va nuestro INSERT con PreparedStatement.
}
@Override
public List<Producto> listar() throws SQLException {
// Aquí va nuestro SELECT y recorrido de ResultSet.
return new ArrayList<Producto>();
}
@Override
public void modificar(Producto producto) throws SQLException {
// Aquí va nuestro UPDATE.
}
@Override
public void eliminar(long codigo) throws SQLException {
// Aquí va nuestro DELETE.
}
}Los cuerpos son marcadores didácticos, no la versión funcional del DAO. El SQL real es el que estudiamos en el SCORM 04.
@Override indica que el método implementa una operación declarada en la interfaz. Si escribimos mal el nombre o una firma, el compilador puede advertirnos.
La interfaz exige esos métodos, pero no decide cómo los implementamos.
Una variable puede tener el tipo de la interfaz
Hasta ahora utilizábamos una referencia directa a una clase concreta:
ProductoDAO dao = new ProductoDAO();
También podemos escribir:
ProductoPersistencia dao = new ProductoDAO();
¿Qué cambió? El objeto sigue siendo un ProductoDAO. Lo que cambia es el tipo de la variable mediante la cual lo utilizamos.
Desde esa variable podemos invocar los métodos previstos en el contrato:
Producto producto = new Producto(101, "Batman: Año Uno", 790, 8); dao.guardar(producto); List<Producto> productos = dao.listar();
Este es un primer ejemplo de polimorfismo: trabajamos mediante el tipo de una interfaz y Java ejecuta la implementación del objeto concreto.
¿Y si queremos utilizar el mismo contrato para ClienteDAO?
La interfaz anterior menciona Producto en sus métodos. No podemos utilizarla sin cambios para guardar Cliente.
En lugar de copiar una interfaz por cada entidad, podemos utilizar un parámetro de tipo. Lo representaremos con la letra T.
import java.sql.SQLException;
import java.util.List;
public interface CrudDAO<T> {
void guardar(T objeto) throws SQLException;
List<T> listar() throws SQLException;
void modificar(T objeto) throws SQLException;
void eliminar(long codigo) throws SQLException;
}Esto se llama una interfaz genérica. La letra T no es una clase llamada T: representa un tipo que se indicará cuando usemos la interfaz.
Usamos long para el identificador porque en este primer ejemplo ambos DAO compartirían ese tipo de código. Más adelante veremos cómo generalizar también el identificador.
¿Qué representa T en nuestra tienda?
Cuando escribimos:
public class ProductoDAO implements CrudDAO<Producto> {
// Implementaciones con Producto y su tabla.
}Estamos indicando que, para este DAO, T representa Producto. Por tanto, la operación guardar(T objeto) será implementada como guardar(Producto producto).
Podemos imaginar otro DAO:
public class ClienteDAO implements CrudDAO<Cliente> {
// Implementaciones con Cliente y su tabla.
}Este segundo ejemplo es una posibilidad futura: no presupone que ya hayamos creado la clase Cliente. Tampoco significa que los dos DAO ejecuten el mismo SQL.
¿Qué ventaja tiene para ProductoService?
En lugar de declarar un atributo del tipo concreto ProductoDAO, el servicio puede depender de la interfaz:
public class ProductoService {
private CrudDAO<Producto> productoDAO;
public ProductoService(CrudDAO<Producto> productoDAO) {
this.productoDAO = productoDAO;
}
// Los métodos del servicio aplican las reglas del negocio
// y luego solicitan guardar(), listar(), etc.
}Esto se conoce como recibir una dependencia por el constructor. El servicio necesita las operaciones del contrato, pero no decide por sí solo cuál es la implementación concreta.
En Main, que coordina la creación de los componentes, podríamos preparar la implementación:
CrudDAO<Producto> dao = new ProductoDAO(); ProductoService service = new ProductoService(dao);
Conservamos nuestra idea original: Main construye y conecta componentes; ProductoController recibe eventos; ProductoService aplica las reglas; el DAO realiza persistencia.
ProductoService, con sus validaciones y excepciones propias.¿Todas las clases DAO deben tener cuatro métodos?
No. La interfaz debe representar un contrato que realmente necesitemos.
Si un DAO solamente permite consultar una tabla de referencia, obligarlo a implementar eliminar() puede ser una mala decisión. Las interfaces deben diseñarse para las necesidades del programa, no para completar una lista por costumbre.
También debemos recordar que las interfaces no resuelven por sí solas los errores SQL ni aseguran la persistencia: solamente establecen operaciones disponibles. La implementación concreta sigue siendo responsable del JDBC, el cierre de recursos y el acceso a datos.
Comprueba lo aprendido
Selecciona una respuesta y revisa la devolución.
1. ¿Cuál es el papel principal de CrudDAO?
2. En «CrudDAO<Producto> dao = new ProductoDAO();», ¿qué objeto construimos?
3. ¿Qué significa T en CrudDAO<T>?
4. ¿Qué ventaja tiene declarar CrudDAO<Producto> dentro de ProductoService?
5. ¿Debemos obligar a todos los DAO a implementar CRUD completo?
Documentamos y dejamos abierta una pregunta
Microentrega de documentación
- Explica con tus palabras la diferencia entre el contrato de una interfaz y su implementación en un DAO.
- En el código
CrudDAO<Producto>, identifica qué significa el parámetro de tipo. - Dibuja un esquema con
ProductoService,CrudDAO<Producto>yProductoDAO, y escribe una responsabilidad para cada elemento. - Explica qué ventaja obtendríamos si pudiéramos cambiar el DAO de MySQL por otro que conserve temporalmente los productos en una lista.
interface, implements y @Override; estándar: explicar y aplicar CrudDAO<Producto>; desafío: proponer cómo recibir distintos DAO por el constructor y anticipar un caso en el que convenga separar contratos.La próxima pregunta: si ProductoService recibe una interfaz, ¿podemos cambiar la forma de guardar productos sin modificar sus reglas de negocio? Esa será una excelente oportunidad para profundizar el polimorfismo y el bajo acoplamiento con dos implementaciones reales.

