En el análisis del código fuente de Mybatis, se pueden identificar varios patrones de diseño que son fundamentales para su arquitectura. Este artículo explora los patrones más relevantes implementados en Mybatis, utilizando ejemplos prácticos para su comprensión.
- Patrón Decorador ================
El patrón decorador permite añadir funcionalidades a un objeto existente sin modificar su estructura. Es útil cuando se tienen operaciones que pueden ser combinadas de diferentes maneras.
1.1 Ejemplo clásico
Un ejemplo clásico del patrón decorador en la API de Java son las clases de entrada/salida. Por ejemplo, para leer un archivo con bufferización:
InputStream flujoEntrada = new FileInputStream("/ruta/archivo.txt");
InputStream flujoBufferizado = new BufferedInputStream(flujoEntrada);
byte[] datos = new byte[128];
while (flujoBufferizado.read(datos) != -1) {
// Procesamiento de datos
}
1.2 Implementación en Mybatis
El sistema de caché de Mybatis utiliza el patrón decorador extensivamente. En lugar de crear una clase monolítica con todas las funcionalidades de caché, Mybatis implementa múltiples decoradores especializados:
- BlockingCache: Implementa un mecanismo de bloqueo cuando no se encuentra un valor en caché.
- FifoCache: Elimina elementos según el principio FIFO (First In, First Out).
- LoggingCache: Registra estadísticas de uso y命中率 (hit rate).
- LruCache: Implementa la política de eliminación LRU (Least Recently Used).
- ScheduledCache: Limpia la caché periódicamente.
- SerializedCache: Serializa los valores para evitar modificaciones externas.
- SoftCache: Utiliza referencias suaves para la gestión de memoria.
- SynchronizedCache: Asegura el acceso thread-safe.
- TransactionalCache: Gestiona la caché en el contexto de transacciones.
- WeakCache: Utiliza referencias débiles.
BaseCache es la clase base fundamental que actúa como un contenedor simple (similar a un HashMap):
public class BaseCache implements Cache {
private final String identificador;
private Map<Object, Object> almacenamiento = new HashMap<>();
public BaseCache(String id) {
this.identificador = id;
}
// Métodos básicos de implementación
}
LruCache es un decorador que envuelve una caché existente para implementar la política LRU:
public class LruCache implements Cache {
private final Cache decorado;
private Map<Object, Object> mapaClaves;
private Object claveMasAntigua;
public LruCache(Cache decorado) {
this.decorado = decorado;
configurarTamanio(1024);
}
// Implementación de la política LRU
}
Durante la inicialización, Mybatis construye una cadena de decoradores según la configuración:
private Cache establecerDecoradoresEstandard(Cache cache) {
try {
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (tamanio != null && metaCache.hasSetter("tamanio")) {
metaCache.setValue("tamanio", tamanio);
}
if (intervaloLimpieza != null) {
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setIntervaloLimpieza(intervaloLimpieza);
}
if (lecturaEscritura) {
cache = new SerializedCache(cache);
}
cache = new LoggingCache(cache);
cache = new SynchronizedCache(cache);
if (bloqueo) {
cache = new BlockingCache(cache);
}
return cache;
} catch (Exception e) {
throw new CacheException("Error construyendo decoradores de caché estándar. Causa: " + e, e);
}
}
- Patrón Método Plantilla ====================
El patrón método plantilla define el esqueleto de un algoritmo en una operación, delegando algunos pasos a las subclases. Permite que las subclases redefinan ciertos pasos de un algoritmo sin cambiar su estructura.
2.1 Ejemplo clásico
En la API de Java, la clase AbstractList implementa el patrón método plantilla. El método addAll define el algoritmo genarel, delegando la implementación del método add a las subclases:
public boolean addAll(int indice, Collection<? extends E> c) {
verificarRangoParaAgregar(indice);
boolean modificado = false;
for (E e : c) {
agregar(indice++, e);
modificado = true;
}
return modificado;
}
public void agregar(int indice, E elemento) {
throw new UnsupportedOperationException();
}
2.2 Implementación en Mybatis
En Mybatis, el manejo de tipos de datos utiliza el patrón método plantilla. La clase BaseTypeHandler define un flujo de trabajo común para convertir entre tipos Java y tipos de base de datos:
@Override
public void setParameter(PreparedStatement ps, int i, T parametro, JdbcType tipoJdbc) throws SQLException {
if (parametro == null) {
if (tipoJdbc == null) {
throw new TypeException("JDBC requiere que se especifique el JdbcType para todos los parámetros nulos.");
}
try {
ps.setNull(i, tipoJdbc.TYPE_CODE);
} catch (SQLException e) {
throw new TypeException("Error estableciendo nulo para el parámetro #" + i + " con JdbcType " + tipoJdbc + ". " +
"Intente establecer un JdbcType diferente para este parámetro o una propiedad jdbcTypeForNull diferente. " +
"Causa: " + e, e);
}
} else {
try {
setNonNullParameter(ps, i, parametro, tipoJdbc);
} catch (Exception e) {
throw new TypeException("Error estableciendo no nulo para el parámetro #" + i + " con JdbcType " + tipoJdbc + ". " +
"Intente establecer un JdbcType diferente para este parámetro o una propiedad de configuración diferente. " +
"Causa: " + e, e);
}
}
}
@Override
public T getResult(ResultSet rs, int columnIndex) throws SQLException {
try {
return getNullableResult(rs, columnIndex);
} catch (Exception e) {
throw new ResultMapException("Error al intentar obtener la columna #" + columnIndex + " del conjunto de resultados. Causa: " + e, e);
}
}
public abstract void setNonNullParameter(PreparedStatement ps, int i, T parametro, JdbcType tipoJdbc) throws SQLException;
public abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException;
public abstract T getNullableResult(ResultSet rs, int columnIndex) throws SQLException;
public abstract T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException;
2.3 Recomendaciones de implementación
- Utilizar la palabra clave final para los métodos plantilla para evitar que las subclases los sobreescriban.
- Declarar los pasos del algoritmo como abstractos o lanzar excepciones forzando su implementación en subclases.
- Patrón Cadena de Responsabilidad ==========================
El patrón cadena de responsabilidad crea una cadena de objetos procesadores, donde cada objeto decide procesar la solicitud o pasarla al siguiente en la cadena.
3.1 Ejemplo clásico
Un ejemplo clásico es el manejo de filtros en Servlets. Cada filtro decide si procesar la solicitud o pasarla al siguiente en la cadena:
@Override
public void doFilter( ServletRequest solicitud, ServletResponse respuesta, FilterChain cadenaFiltros )
throws IOException, ServletException{
// Procesamiento previo
cadenaFiltros.doFilter( solicitud, respuesta );
// Procesamiento posterior
}
3.2 Implementación en Mybatis
El sistema de plugins de Mybatis utiliza el patrón cadena de responsabilidad para permitir la extensión de funcionalidades. Cada plugin puede interceptar y modificar el comportamiento de ciertos componentes de Mybatis:
public class CadenaInterceptores {
private final List<Interceptor> interceptores = new ArrayList<>();
/**
* Procesa un objeto objetivo a través de todos los interceptores
* @param objetivo El objeto a procesar
* @return El objeto procesado (posiblemente un proxy)
*/
public Object procesarTodos(Object objetivo) {
for (Interceptor interceptor : interceptores) {
objetivo = interceptor.procesar(objetivo);
}
return objetivo;
}
public void agregarInterceptor(Interceptor interceptor) {
interceptores.add(interceptor);
}
public List<Interceptor> getInterceptores() {
return Collections.unmodifiableList(interceptores);
}
}
3.3 Diferencias entre implementaciones
Existen variaciones en cómo se implementa la cadena de responsabilidad. En el caso de los filtros de Servlet, cada filtro debe explícitamente invocar el siguiente en la cadena, permitiendo la posibilidad de cortocircuitar el flujo. En el caso de los plugins de Mybatis, el procesamiento es secuencial y no permite cortocircuitar.
- Patrón Proxy ============
El patrón proxy proporciona un sustituto o marcador de posición para otro objeto para controlar el acceso a él.
4.1 Ejemplo clásico
Un ejemplo de proxy dinámico con JDK:
public class ProxyRecolectorMetricas {
private RecolectorMetricas recolector;
public ProxyRecolectorMetricas() {
this.recolector = new RecolectorMetricas();
}
public Object crearProxy(Object objetoProxiado) {
Class<?>[] interfaces = objetoProxiado.getClass().getInterfaces();
ManejadorProxyDinamico manejador = new ManejadorProxyDinamico(objetoProxiado);
return Proxy.newProxyInstance(objetoProxiado.getClass().getClassLoader(), interfaces, manejador);
}
private class ManejadorProxyDinamico implements InvocationHandler {
private Object objetoProxiado;
public ManejadorProxyDinamico(Object objetoProxiado) {
this.objetoProxiado = objetoProxiado;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long inicioTiempo = System.currentTimeMillis();
Object resultado = method.invoke(objetoProxiado, args);
long finTiempo = System.currentTimeMillis();
long tiempoRespuesta = finTiempo - inicioTiempo;
String nombreApi = objetoProxiado.getClass().getName() + ":" + method.getName();
InfoSolicitud infoSolicitud = new InfoSolicitud(nombreApi, tiempoRespuesta, inicioTiempo);
recolector.registrarSolicitud(infoSolicitud);
return resultado;
}
}
}
4.2 Implementación en Mybatis
Mybatis utiliza intensivamente el patrón proxy, especialmente en la implementación de los Mapper interfaces. Cuando se define un Mapper interface, Mybatis crea dinámicamente un proxy que implementa los métodos del interface y los asocia a las sentencias SQL correspondientes:
public class ProxyMapper<T> implements InvocationHandler, Serializable {
private static final long serialVersionUID = -6424540398559729838L;
private final SqlSession sqlSession;
private final Class<T> interfaceMapper;
private final Map<Method, MetodoMapper> cacheMetodos;
public ProxyMapper(SqlSession sqlSession, Class<T> interfaceMapper, Map<Method, MetodoMapper> cacheMetodos) {
this.sqlSession = sqlSession;
this.interfaceMapper = interfaceMapper;
this.cacheMetodos = cacheMetodos;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (method.isDefault()) {
return invocarMetodoPorDefecto(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.desenvolverExcepcion(t);
}
MetodoMapper metodoMapper = obtenerMetodoMapper(method);
return metodoMapper.ejecutar(sqlSession, args);
}
private MetodoMapper obtenerMetodoMapper(Method method) {
return cacheMetodos.computeIfAbsent(method, k -> new MetodoMapper(interfaceMapper, method, sqlSession.getConfiguration()));
}
private Object invocarMetodoPorDefecto(Object proxy, Method method, Object[] args) throws Throwable {
Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class
.getDeclaredConstructor(Class.class, int.class);
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
Class<?> claseDeclarante = method.getDeclaringClass();
return constructor
.newInstance(claseDeclarante,
MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
| MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC)
.unreflectSpecial(method, claseDeclarante).bindTo(proxy).invokeWithArguments(args);
}
}
La fábrica que crea estos proxies:
public class FabricaProxyMapper<T> {
private final Class<T> interfaceMapper;
public FabricaProxyMapper(Class<T> interfaceMapper) {
this.interfaceMapper = interfaceMapper;
}
@SuppressWarnings("unchecked")
protected T crearInstancia(ProxyMapper<T> proxyMapper) {
return (T) Proxy.newProxyInstance(interfaceMapper.getClassLoader(), new Class[] { interfaceMapper }, proxyMapper);
}
public T crearInstancia(SqlSession sqlSession) {
final ProxyMapper<T> proxyMapper = new ProxyMapper<>(sqlSession, interfaceMapper, new ConcurrentHashMap<>());
return crearInstancia(proxyMapper);
}
}
El registro de Mappers en Mybatis:
public <T> void agregarMapper(Class<T> tipo) {
if (tipo.isInterface()) {
if (tieneMapper(tipo)) {
throw new BindingException("El tipo " + tipo + " ya es conocido por el MapperRegistry.");
}
boolean cargaCompletada = false;
try {
mappersConocidos.put(tipo, new FabricaProxyMapper<>(tipo));
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, tipo);
parser.parse();
cargaCompletada = true;
} finally {
if (!cargaCompletada) {
mappersConocidos.remove(tipo);
}
}
}
}
- Otros patrones en Mybatis ========================
Además de los patrones ya mencionados, Mybatis utiliza otros patrones de diseño comunes como:
- Patrón Singleton: Para garantizar una única instancia de componentes como SqlSessionFactory.
- Patrón Builder: Para la construcción de objetos complejos como configuraciones.
- Patrón Factory: Para la creación de objetos como Mappers y ResultHandlers.
- Conclusión ===========
- Este artículo ha explorado los patrones de diseño fundamentales en Mybatis: decorador, método plantilla, cadena de responsabilidad y proxy.
- El estudio de patrones en frameworks como Mybatis permite comprender su aplicación práctica en sistemas reales, superando la limitación de ejemplos académicos.
- La implementación adecuada de patrones de diseño mejora la calidad del código, facilitando su extensión y mantenimiento, que es el objetivo principal de su aprendizaje.