(Aviso a navegantes: ¡Este post contiene mucho código escrito a altas horas de la noche!)
Hola de nuevo, en este post, el último sobre «extends», voy a intentar mostrar alternativas a la herencia de implementación. Se trata de la técnica genérica llamada «composición y delegación», técnica en la que se basan varios patrones de diseño, como el famoso «Command», el «Strategy», o el «Decorator» entre otros. Antes de nada voy a resumir lo contado hasta ahora. En el primer post vimos cómo usar la herencia de implementación de forma «ingenua» nos podía meter en un buen lío, ya que sin darnos cuenta podemos romper la encapsulación. En el segundo post intentamos arreglar los problemas usando la palabra reservada «final» y el patrón «Template Method». Si bien al principio parecía que funcionaba, más tarde al ir añadiendo funcionalidades, nos dimos cuenta de que violábamos el principio DRY y se producía una explosión combinatoria de clases, además de dificultarnos el testing y tener que planear de antemano los «ejes» de extensibilidad de nuestra clase.
El problema más importante es la explosión combinatoria de clases y la violación del DRY. La base de este problema es que la lógica de filtrado, y de postprocesamiento de la inserción de elementos, no se encuentran debidamente encapsuladas y abstraídas, sino que están incrustadas dentro de las clases hijas de «ListaSencilla».
Empezando por la lógica de filtrado, ¿no sería mejor si pudiéramos tener una clase donde realmente pudiéramos poner dicha lógica? De esa forma podríamos eliminar ésta de las clases «ListaAuditableConElementosSinEspacios», «ListaAuditableConElementosLongitudPar», «ListaConElementosSinEspacios» y «ListaConElementosLongitudPar». Está claro que estas clases violan el principio de única responsabilidad, ya que implementan el contrato de «Lista» y además tienen lógica de filtrado. Si sacamos esa lógica de filtrado aparte, vamos a quitar responsabilidad de dichas clases y a simplificarlas.
Lo primero de todo sería definir una interfaz «EstrategiaDeFiltrado» que represente el contrato entre una implementación de «Lista» y la lógica de filtrado.
public interface EstrategiaDeFiltrado {
boolean esInsertable(String elemento);
}
Sencillo, ¿verdad? Ahora sólo tengo que tener una implementación por estragia de filtrado en mi sistema. Primero para los elementos pares:
public final class FiltrarElementosLongitudImpar implements EstrategiaDeFiltrado {
public FiltrarElementosLongitudImpar() {
super();
}
@Override
public boolean esInsertable(String elemento) {
return elemento.length() % 2 == 0;
}
}
Y ahora para los elementos con espacios:
public final class FiltrarElementosConEspacios implements EstrategiaDeFiltrado {
public FiltrarElementosConEspacios() {
super();
}
@Override
public boolean esInsertable(String elemento) {
return !elemento.contains(" ");
}
}
Nótese ese «final» para cada clase. Ahora nadie podrá usar herencia de implementación de dichas clases y provocar el caos. Es también interesante notar que con este diseño puedo hacer unos tests muy sencillos de cada implementación de «EstrategiaDeFiltrado». Esto pinta bien, pero aun no hemos arreglado nada. Vamos a ver como reutilizar estas implementaciones de «EstrategiaDeFiltrado» en nuestras listas. Primero un cambio pequeño en «ListaAuditableConElementosLongitudPar»:
public class ListaAuditableConElementosLongitudPar extends ListaAuditable {
private EstrategiaDeFiltrado filtro = new FiltrarElementosLongitudImpar();
public ListaAuditableConElementosLongitudPar(Auditor auditor) {
super(auditor);
}
@Override
protected boolean esInsertable(String elemento) {
return filtro.esInsertable(elemento);
}
}
y lo mismo en «ListaAuditableConElementosSinEspacios»:
public class ListaAuditableConElementosSinEspacios extends ListaAuditable {
private EstrategiaDeFiltrado filtro = new FiltrarElementosConEspacios();
public ListaAuditableConElementosSinEspacios(Auditor auditor) {
super(auditor);
}
@Override
protected boolean esInsertable(String elemento) {
return filtro.esInsertable(elemento);
}
}
Mmmm, si os fijais «ListaAuditableConElementosLongitudPar» y «ListaAuditableConElementosSinEspacios» tienen exactamente el mismo código, salvo que cada una usa una implementación concreta diferente de «EstragiaDeFiltrado». Una violación del DRY muy clara. Además ese «new» es muy, pero que muy feo ¿No sería mejor pasar el colaborador, es decir, la implementación concreta de «EstrategiaDeFiltrado», por el constructor? Esto nos elimina la duplicación y de paso podemos borrar una clase. ¡ Me encanta cuando puedo borrar código ! ¿Y a vosotros? Ahora ambas clases desaparecen y son sustituidas por «ListaAuditableConFiltro»:
public class ListaAuditableConFiltro extends ListaAuditable {
private EstrategiaDeFiltrado filtro;
public ListaAuditableConFiltro(Auditor auditor, EstrategiaDeFiltrado filtro) {
super(auditor);
this.filtro = filtro;
}
@Override
protected boolean esInsertable(String elemento) {
return filtro.esInsertable(elemento);
}
}
De forma análoga, «ListaConElementosLongitudPar» y «ListaConElementosSinEspacios» acaban siendo eliminadas y sustituidas por «ListaConFiltro». Esto no me termina de convencer. Seguimos violando DRY. Por un lado «ListaConFiltro» y «ListaAuditableConFiltro» tienen lógica claramente duplicada. Por otro lado, el método protegido «esInsertable» es exactamente igual que la interfaz «EstrategiaDeFiltrado». El código pide a gritos ser simplificado, así que, ¿qué tal si nos llevamos todo ese código común a la superclase «ListaSencilla»? El único problema es que de «ListaSencilla» heredan también listas que no tienen filtro, ¿qué hacemos? Podemos aplicar el patrón «null object» y crear una «EstrategiaDeFiltrado» que no haga nada. Veamos ese código:
public final class NuncaFiltrar implements EstrategiaDeFiltrado {
public NuncaFiltrar() {
super();
}
@Override
public boolean esInsertable(String elemento) {
return true;
}
}
Por otro lado no queremos tener que configurar el colaborador filtro con la clase «NuncaFiltrar» cada vez que queramos una lista sin filtro. Por lo tanto necesitamos que el filtro sea una dependencia opcional dentro de «ListaSencilla». El código quedaría:
public class ListaSencilla implements Lista {
private List datos = new ArrayList();
private EstrategiaDeFiltrado filtro = new NuncaFiltrar();
public ListaSencilla() {
super();
}
@Override
final public void insertar(String elemento) {
if (!filtro.esInsertable(elemento))
throw new ElementoRechazadoError(elemento);
datos.add(elemento);
procesamientoAdicionalTrasInsertarElemento(elemento);
}
@Override
final public void insertarVarios(Collection elementos) {
for (String elemento : elementos) {
if (!filtro.esInsertable(elemento))
throw new ElementoRechazadoError(elemento);
}
for (String elemento : elementos)
insertar(elemento);
}
public final void configurarFiltro(EstrategiaDeFiltrado nuevoFiltro) {
this.filtro = nuevoFiltro;
}
/**
* Este método es invocado cada vez que un nuevo elemento
* es insertado en la Lista
*
* @param elemento
* El elemento que acaba de ser insertado
*/
protected void procesamientoAdicionalTrasInsertarElemento(String elemento)
{
// Intencionadamente en blanco
}
@Override
public String describirContenido() {
StringBuilder descripcion = new StringBuilder("Contenidos: ");
for (String elemento : datos) {
descripcion.append("'");
descripcion.append(elemento);
descripcion.append("' ");
}
return descripcion.toString();
}
}
Obsérvese como se recibe el colaborador mediante un «setter» llamado «configurarFiltro», y no por el constructor. También como se usa por defecto una instancia de «NuncaFiltrar» en caso de que no se configure nada. También hemos eliminado el método protegido «esInsertable».
Finalmente hemos conseguido nuestro objetivo, hemos eliminado todas las subclases de «ListaSencilla» que creamos con el único fin de añadir lógica de filtrado. El patrón que estamos usando es el «Strategy» tal y como apareción en GoF. Podemos volver a usar el patrón «Strategy» para eliminar «ListaAuditable» y el método protegido «procesamientoAdicionalTrasInsertarElemento». Para ello creamos una interfaz «PostProcesador»:
public interface PostProcesador {
void postProcesar(String elemento);
}
y una implementación «Auditar»:
public class Auditar implements PostProcesador {
private Auditor auditor;
public Auditar(Auditor auditor) {
super();
this.auditor = auditor;
}
@Override
public void postProcesar(String elemento) {
auditor.elementoInsertado(elemento);
}
}
Obsérvese que «Auditar» no es más que un patrón «Adapter», entre la interfaz «PostProcesador» y «Auditor». Además sospechosamente «Auditar» se parece mucho a «ListaAuditable». Finalmente «ListaSencilla» queda:
public final class ListaSencilla implements Lista {
private List datos = new ArrayList();
private EstrategiaDeFiltrado filtro = new NuncaFiltrar();
private PostProcesador postProcesador = new NoProcesar();
public ListaSencilla() {
super();
}
@Override
public void insertar(String elemento) {
if (!filtro.esInsertable(elemento))
throw new ElementoRechazadoError(elemento);
datos.add(elemento);
postProcesador.postProcesar(elemento);
}
@Override
public void insertarVarios(Collection elementos) {
for (String elemento : elementos) {
if (!filtro.esInsertable(elemento))
throw new ElementoRechazadoError(elemento);
}
for (String elemento : elementos)
insertar(elemento);
}
public void configurarFiltro(EstrategiaDeFiltrado nuevoFiltro) {
this.filtro = nuevoFiltro;
}
public void configurarPostProcesador(PostProcesador nuevoPostProcesador) {
this.postProcesador = nuevoPostProcesador;
}
@Override
public String describirContenido() {
StringBuilder descripcion = new StringBuilder("Contenidos: ");
for (String elemento : datos) {
descripcion.append("'");
descripcion.append(elemento);
descripcion.append("' ");
}
return descripcion.toString();
}
}
Ahora «ListaSencilla» es «final», sin ningún método protegido y hemos eliminado «ListaAuditable». Hemos conseguido cubrir todos nuestros requisitos sin necesitar la «potencia» de la herencia de implementación. Además hemos simplificado los tests. Es muy sencillo hacer tests de las implementaciones de «EstrategiaDeFiltrado» y «PostProcesador». También los tests de «ListaSencilla» pueden ser simplificados usando dobles de prueba.
Sólo nos queda una cosa, el famoso patrón «Factory»:
public final class NuevaLista {
public NuevaLista() {
super();
}
private ListaSencilla configurarAuditorEstricto(ListaSencilla lista) {
lista.configurarPostProcesador(new Auditar(new AuditorEstricto()));
return lista;
}
private ListaSencilla configurarFiltro(ListaSencilla lista,
EstrategiaDeFiltrado filtro) {
lista.configurarFiltro(filtro);
return lista;
}
public Lista sencilla() {
return new ListaSencilla();
}
public Lista auditable() {
return configurarAuditorEstricto(new ListaSencilla());
}
public Lista sinElementosConEspacios() {
return configurarFiltro(new ListaSencilla(),
new FiltrarElementosConEspacios());
}
public Lista sinElementosLongitudImpar() {
return configurarFiltro(new ListaSencilla(),
new FiltrarElementosLongitudImpar());
}
public Lista auditableSinElementosConEspacios() {
return configurarAuditorEstricto(configurarFiltro(new ListaSencilla(),
new FiltrarElementosConEspacios()));
}
public Lista auditableSinElementosLongitudImpar() {
return configurarAuditorEstricto(configurarFiltro(new ListaSencilla(),
new FiltrarElementosLongitudImpar()));
}
}
O si son ustedes partidarios de usar frameworks, pueden usar el framework «Spring» que da mucho juego para esto, y programarse exactamente la misma lógica que tenemos en «NuevaLista» pero en XML en vez de en JAVA (es que el XML queda más profesional). Con este patrón «Factory» podemos tener todas las combinaciones de «Lista», «EstrategiaDeFiltrado» y «PostProcesador» que queramos, sin necesidad de volver a tocar una linea de código en ninguna de estas.
El patrón «Strategy» nos permite hacer una cosa que no podemos con «extends», definir la combinación que queramos de «Lista», «EstrategiaDeFiltrado» y «PostProcesador» en tiempo de ejecución.
Podríamos dejarlo aquí, pero he de comentar que «Strategy» comparte un problema con «Template Method». Con ambos necesitamos planear de antemano que «ejes» de extensibilidad o composición vamos a tener. Esto hace que la clase «ListaSencilla» no sea tan sencilla. Estamos en cierto modo violando el principio de única responsabilidad, ya que está claro que la responsabilidad de «ListaSencilla» es simplemente gestionar la lógica de «Lista». Además, estamos pagando una pequeña sobrecarga, al añadir lógica de llamada a filtrado y postprocesamiento en aquellos casos que realmente no la necesitamos. Cierto, es una sobrecarga muy pequeña, pero no deja de ser una mala señal. Yo lo que quiero es que «ListaSencilla» sea eso, «sencilla». Quiero este código:
public final class ListaSencilla implements Lista {
private List datos = new ArrayList();
public ListaSencilla() {
super();
}
@Override
public void insertar(String elemento) {
datos.add(elemento);
}
@Override
public void insertarVarios(Collection elementos) {
for (String elemento : elementos)
insertar(elemento);
}
@Override
public String describirContenido() {
StringBuilder descripcion = new StringBuilder("Contenidos: ");
for (String elemento : datos) {
descripcion.append("'");
descripcion.append(elemento);
descripcion.append("' ");
}
return descripcion.toString();
}
}
¡ Ah, que gusto volver a los viejos tiempos donde las listas eran listas y nada más ! Pero necesitamos filtrado y postprocesamiento, ¿qué hacemos? Recurrir a otro patrón del «GoF», el «Decorator». Podemos hacer una clase que admita una «EstrategiaDeFiltrado», pero que no implemente nada de la lógica de la lista, sino que la delegue a un colaborador. O sea, la responsabilidad de esta nueva clase es simplemente orquestar a una «Lista» y a una «EstrategiaDeFiltrado», sin que ninguna de las dos sepa nada. Creemos, para tal efecto, la clase «ListaConFiltrado»:
public final class ListaConFiltrado implements Lista {
private Lista lista;
private EstrategiaDeFiltrado filtro;
public ListaConFiltrado(Lista lista, EstrategiaDeFiltrado filtro) {
super();
this.lista = lista;
this.filtro = filtro;
}
@Override
public void insertar(String elemento) {
if (!filtro.esInsertable(elemento))
throw new ElementoRechazadoError(elemento);
lista.insertar(elemento);
}
@Override
public void insertarVarios(Collection elementos) {
for (String elemento : elementos) {
if (!filtro.esInsertable(elemento))
throw new ElementoRechazadoError(elemento);
}
lista.insertarVarios(elementos);
}
@Override
public String describirContenido() {
return lista.describirContenido();
}
}
También podemos hacer lo mismo y crear una «ListaConPostProcesamiento»:
public final class ListaConPostProcesamiento implements Lista {
private Lista lista;
private PostProcesador postProcesador;
public ListaConPostProcesamiento(Lista lista, PostProcesador postProcesador) {
super();
this.lista = lista;
this.postProcesador = postProcesador;
}
@Override
public void insertar(String elemento) {
lista.insertar(elemento);
postProcesador.postProcesar(elemento);
}
@Override
public void insertarVarios(Collection elementos) {
lista.insertarVarios(elementos);
for (String elemento : elementos)
postProcesador.postProcesar(elemento);
}
@Override
public String describirContenido() {
return lista.describirContenido();
}
}
¿Necesitaremos una clase «ListaConFiltradoYPostProcesamiento»? No, tal objeto se puede crear componiendo una «ListaConFiltrado» con una «ListaConPostProcesamiento», pasando la última como argumento al constructor de la primera. De hecho ahora podemos eliminar «NuncaFiltrar» y «NoProcesar» ya que no las usamos. Además volvemos a simplificar los tests de «ListaSencilla», ¡ ya no necesitamos dobles de prueba ! Por otro lado los tests de «ListaConFiltrado» y «ListaConPostProcesamiento» son muy sencillos, basta usar unos dobles de prueba para comprobar que las llamadas se delegan en el orden oportuno, y que no se llama ni a «insertar» ni a «insertarVarios» si el filtro nos devuelve «false».
Si os fijais estoy usando varios patrones que no usan herencia de implementación, sino una filosofía de diseño general llamada «composición y delegación«. Esta filosofía consiste en tener objetos sencillos, y bien encapsulados, que puedan ser combinados mediante composición, para obtener nueva funcionalidad. Tanto «Strategy», como «Decorator» usan esta técnica.
Siguiendo con el ejemplo, todos estos cambios afectan a nuestra factoría «NuevaLista», que ahora queda:
public final class NuevaLista {
public NuevaLista() {
super();
}
public Lista sencilla() {
return new ListaSencilla();
}
public Lista auditable() {
return new ListaConPostProcesamiento(sencilla(), new Auditar(
new AuditorEstricto()));
}
public Lista sinElementosConEspacios() {
return new ListaConFiltrado(sencilla(), new FiltrarElementosConEspacios());
}
public Lista sinElementosLongitudImpar() {
return new ListaConFiltrado(sencilla(), new FiltrarElementosLongitudImpar());
}
public Lista auditableSinElementosConEspacios() {
return new ListaConFiltrado(auditable(), new FiltrarElementosConEspacios());
}
public Lista auditableSinElementosLongitudImpar() {
return new ListaConFiltrado(auditable(),
new FiltrarElementosLongitudImpar());
}
}
Ahora ese «Factory» es más sencillo y elegante que antes al haber eliminado los «setter». La clase «NuevaLista» simplemente compone o combina las distintas implementaciones de lista. Desgraciadamente cada vez que necesitemos una nueva combinación tenemos que tocar «NuevaLista» (o el XML de «Spring»). Para solventar ese problema podríamos aprovechar la potencia de «componer y delegar» y hacernos un mini DSL interno. Veamos cómo:
public final class FabricaDeListas {
private List<EstrategiaDeFiltrado> filtros = new ArrayList();
private List<PostProcesador> postProcesadores = new ArrayList();
public FabricaDeListas() {
super();
}
public FabricaDeListas(List filtros,
List postProcesadores) {
this();
this.filtros.addAll(filtros);
this.postProcesadores.addAll(postProcesadores);
}
public Lista fabricar() {
Lista resultado = new ListaSencilla();
for (PostProcesador procesador : postProcesadores)
resultado = new ListaConPostProcesamiento(resultado, procesador);
for (EstrategiaDeFiltrado filtro : filtros)
resultado = new ListaConFiltrado(resultado, filtro);
return resultado;
}
public FabricaDeListas conPostProcesador(PostProcesador procesador) {
FabricaDeListas fabrica = new FabricaDeListas(this.filtros,
this.postProcesadores);
fabrica.postProcesadores.add(procesador);
return fabrica;
}
public FabricaDeListas conAuditor(Auditor auditor) {
return conPostProcesador(new Auditar(auditor));
}
public FabricaDeListas conAuditoriaEstricta() {
return conAuditor(new AuditorEstricto());
}
public FabricaDeListas conFiltro(EstrategiaDeFiltrado filtro) {
FabricaDeListas fabrica = new FabricaDeListas(this.filtros,
this.postProcesadores);
fabrica.filtros.add(filtro);
return fabrica;
}
public FabricaDeListas sinElementosLongitudImpar() {
return conFiltro(new FiltrarElementosLongitudImpar());
}
public FabricaDeListas sinElementosConEspacios() {
return conFiltro(new FiltrarElementosConEspacios());
}
}
Simplemente usamos un patrón «Value Object» mezclado con un «Builder». La ventaja de este enfoque es que sea cual sea la combinación que quiera la puedo obtener fácilmente con «FabricaDeListas», sin tener que tocar una sola linea de código dentro de «FabricaDeListas». Puedo hacer cosas como:
Lista auditadaSinElementosConEspacios = fabrica
.sinElementosConEspacios()
.conAuditoriaEstricta()
.fabricar();
Mucho más legible, ¿no?
Tras este largo camino, ¿alguien considera que el código resultante es ilegible, poco mantenible o poco potente? ¿Para que nos sirvió el «extends» si no fue sólo para darnos dolores de cabeza? Hemos aprendido una valiosa lección, cualquier diseño basado en «extends» y «Template Method» puede ser refactorizado en un «Strategy», y éste a su vez en un «Decorator». Por lo tanto, ¿para que perder el tiempo usando «extends» si puedo hacer un «Strategy» o un «Decorator»? Al fin y al cabo el diseño basado en «composición y delegación» es más seguro y más potente.
Sin embargo aquí entra en juego las herramientas que te da el lenguaje. Una pista nos la puede dar la clase «Auditar», donde estamos creando una clase para simplemente adaptar la interfaz «PostProcesador» a «Auditor», pero realmente no hay ninguna lógica ahí. A mi me parece que esa clase es un buen desperdicio de código, pero la verdad es que no se puede hacer mucho más en JAVA. Otro ejemplo es el método «describirContenido» en «ListaConFiltrado» y «ListaConPostProcesamiento». Ese método no hace nada, sólo delega la llamada sin añadir lógica, ¡ qué desperdicio de líneas de código ! Tampoco podemos hacer mucho más en leguaje JAVA. Afortunadamente tenemos IDEs como «Eclipse», «NetBeans» o «IntelliJ» que nos proporcionan el poderoso wizard «Generate Delegate Methods…», y no las tenemos que escribir.
Cuando veo este tipo de detalles es cuando pienso que JAVA no está envejeciendo muy bien. Gracias señores estandarizadores de JAVA, perdieron ustedes una gran oportunidad de mejorar el lenguaje cuando lanzaron JAVA 5. En vez de parir ese aborto de «Map<? extends Enumerable, List<? extends X>>», debían haber mejorado el lenguaje para añadir una forma sencilla de aplicar la técnica de «composición y delegación», ¿tal vez una palabra clave «delegate»? ¿O soporte para funciones como primer ciudadano? No, ¿para qué? Los genéricos son mucho más «molones» y para reutilizar código ya tenemos el «extends». Todo esto es mucho más simple de hacer en JavaScript, Groovy o Ruby. A ver si en JAVA 7 o JAVA 8 se ponen las pilas.
¡ Gracias por aguantar hasta el final de este post tan largo !