Una pregunta para poner a prueba las interfaces
Ya logramos separar las responsabilidades: ProductoController coordina la vista, ProductoService comprueba las reglas de negocio y ProductoDAO guarda los productos en MySQL.
En el SCORM 05 definimos un contrato CrudDAO<Producto>. Pero podríamos preguntarnos: ¿para qué nos sirve en la práctica?
Para comprender la respuesta construiremos una segunda implementación del mismo contrato y observaremos cómo Java decide qué métodos ejecutar.
Lo que la interfaz promete y lo que no promete
Recordemos el contrato del material anterior. En este ejemplo conservamos las cuatro operaciones y el identificador long de nuestros productos:
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;
}Este código no abre conexiones, no contiene un ArrayList y no ejecuta SQL. Define qué operaciones debe ofrecer una clase que implemente el contrato.
También mantiene throws SQLException para que sea compatible con la implementación JDBC que ya estudiamos. Más adelante veremos que, si diseñáramos la aplicación desde cero para admitir muchos mecanismos de persistencia, podríamos preferir excepciones propias de la capa de datos.
Dos clases, el mismo contrato, distinto trabajo
Podemos tener dos clases que implementan CrudDAO<Producto>:
public class ProductoDAO implements CrudDAO<Producto> {
// Utiliza Connection, PreparedStatement y MySQL.
}
public class ProductoDAOEnMemoria implements CrudDAO<Producto> {
// Utiliza un ArrayList<Producto>.
}Estos fragmentos muestran la declaración de cada clase; todavía no son implementaciones completas.
Las dos prometen guardar(), listar(), modificar() y eliminar(). Sin embargo, la primera se comunica con una base de datos y la segunda solo modifica una colección que vive en la memoria.
La implementación en memoria será temporal: al cerrar el programa se perderán sus datos. No resuelve la persistencia; sirve para comprender la sustitución y para determinadas pruebas.
Construimos una implementación en memoria
Esta clase implementa el contrato sin utilizar JDBC. Observa sus dos primeras operaciones:
import java.util.ArrayList;
import java.util.List;
public class ProductoDAOEnMemoria implements CrudDAO<Producto> {
private ArrayList<Producto> productos;
public ProductoDAOEnMemoria() {
productos = new ArrayList<Producto>();
}
@Override
public void guardar(Producto producto) {
productos.add(producto);
}
@Override
public List<Producto> listar() {
return new ArrayList<Producto>(productos);
}
// Aún faltan modificar() y eliminar().
}Este fragmento es una etapa de construcción: hasta implementar modificar() y eliminar(), la clase no compilará como clase concreta. Los completaremos en el siguiente apartado.
guardar() incorpora el objeto a la lista. listar() devuelve una nueva lista con las mismas referencias a productos: evita exponer directamente la colección interna, aunque no copia profundamente cada objeto.
Observa también que aquí no escribimos throws SQLException. Una implementación puede declarar menos excepciones comprobadas que el contrato.
Completamos las otras dos operaciones
Dentro de ProductoDAOEnMemoria incorporamos los métodos que faltaban:
@Override
public void modificar(Producto producto) {
for (int i = 0; i < productos.size(); i++) {
if (productos.get(i).getCodigo() == producto.getCodigo()) {
productos.set(i, producto);
break;
}
}
}
@Override
public void eliminar(long codigo) {
for (int i = 0; i < productos.size(); i++) {
if (productos.get(i).getCodigo() == codigo) {
productos.remove(i);
break;
}
}
}Ahora, al reunir ambos fragmentos dentro de la clase, tenemos los cuatro métodos exigidos por CrudDAO<Producto>.
modificar() busca un producto por código y reemplaza el elemento de la lista. eliminar() busca el código y quita el elemento correspondiente. Utilizamos break porque el código identifica un producto y no necesitamos seguir recorriendo.
Cuando el servicio crea su propio DAO
Imaginemos que ProductoService decide directamente qué implementación utilizar:
public class ProductoService {
private ProductoDAO productoDAO;
public ProductoService() {
productoDAO = new ProductoDAO();
}
}El servicio conoce la clase concreta ProductoDAO y la construye él mismo. Para sustituir MySQL por nuestra lista, tendríamos que modificar el código de ProductoService.
Se dice que el servicio está acoplado a esa implementación concreta. El problema no es que existan dependencias —todos los programas las tienen—, sino de qué depende cada clase.
El servicio recibe la dependencia por el constructor
Podemos cambiar el diseño para que el servicio reciba un objeto a través del constructor, utilizando el tipo de la interfaz:
public class ProductoService {
private CrudDAO<Producto> productoDAO;
public ProductoService(CrudDAO<Producto> productoDAO) {
this.productoDAO = productoDAO;
}
public void guardarProducto(Producto producto) throws SQLException {
// Aquí se aplican nuestras reglas de negocio.
productoDAO.guardar(producto);
}
}Este es un fragmento explicativo, no la sustitución literal de nuestro ProductoService, que ya incluye comprobaciones y excepciones propias. Esas reglas deben conservarse.
La instrucción this.productoDAO = productoDAO; guarda la referencia recibida. El servicio no decide si el objeto es JDBC o memoria. Solo sabe que cumple el contrato.
Este mecanismo se denomina inyección de dependencias por constructor. No requiere bibliotecas ni frameworks: utilizamos simplemente un constructor de Java.
Main elige la implementación
Recordemos una decisión de arquitectura de nuestra unidad anterior: Main crea los componentes y los conecta. Por eso es un lugar adecuado para elegir el DAO concreto.
CrudDAO<Producto> dao = new ProductoDAO(); ProductoService servicio = new ProductoService(dao);
CrudDAO<Producto> dao = new ProductoDAOEnMemoria(); ProductoService servicio = new ProductoService(dao);
Compara los fragmentos: cambia la clase que construimos con new, pero no cambia el constructor del servicio.
En ambos casos, el controlador puede continuar comunicándose con el mismo servicio y la vista puede seguir mostrando productos del mismo modelo.
¿Cuál guardar() se ejecuta?
Fíjate en esta línea:
CrudDAO<Producto> dao = new ProductoDAOEnMemoria();
El tipo de la referencia es CrudDAO<Producto>. El tipo del objeto concreto que creamos con new es ProductoDAOEnMemoria.
Cuando más adelante llamamos a dao.guardar(producto), Java utiliza la implementación de guardar() que corresponde al objeto real: en este caso, la del ArrayList.
Si el objeto real fuera ProductoDAO, se ejecutaría su implementación JDBC.
Bajo acoplamiento no significa ausencia de reglas
Puede parecer que cambiar el DAO resuelve automáticamente cualquier problema. No es así.
Imagina que el DAO de MySQL impide guardar dos productos con el mismo código porque la tabla tiene una clave primaria; en cambio, nuestro DAO en memoria utiliza productos.add(producto) y permite repetir códigos.
Ambos implementan los mismos métodos, pero no producen siempre los mismos resultados.
Para que podamos sustituir una implementación por otra sin sorpresas, debemos acordar qué ocurre cuando un código se repite, cuando intentamos modificar un producto inexistente o cuando falla el almacenamiento.
La interfaz expresa las firmas de los métodos, pero no verifica todas esas reglas. Por eso necesitamos documentación, pruebas y responsabilidades claras.
Además, declarar SQLException en el contrato hace que el servicio conozca una excepción de JDBC. Funciona para la etapa actual, pero deja un acoplamiento técnico: en una evolución futura podríamos definir una excepción propia de persistencia y traducir allí los errores SQL.
Comprobamos lo aprendido
Selecciona una respuesta. La devolución explica el razonamiento.
1. ¿Qué tienen en común ProductoDAO y ProductoDAOEnMemoria?
2. ¿Cuál es el tipo más adecuado para el atributo del servicio en este diseño?
3. ¿Dónde elegimos inicialmente si usar el DAO JDBC o el DAO en memoria?
4. Si dao contiene un ProductoDAOEnMemoria, ¿qué guardar() se ejecuta?
5. ¿Una interfaz garantiza que dos implementaciones reaccionen igual ante códigos duplicados?
Documentamos lo aprendido y cerramos la unidad
Microentrega de documentación
- Dibuja un esquema que incluya
Main,ProductoService,CrudDAO<Producto>,ProductoDAOyProductoDAOEnMemoria. Señala quién construye a quién. - Explica qué cambia y qué permanece igual cuando sustituimos el DAO JDBC por el DAO en memoria.
- Indica qué implementación conserva los productos después de cerrar el programa y por qué.
- Describe una diferencia de comportamiento que deberíamos corregir para que ambas implementaciones sean realmente sustituibles.
Ya podemos leer nuestra arquitectura de otra manera: una interfaz indica qué necesitamos; una implementación decide cómo hacerlo; y Main conecta los componentes que trabajarán juntos.
Próximo paso: una práctica integradora en la que comprobaremos que el sistema funciona con MySQL y que nuestras responsabilidades permanecen separadas.

