(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 !
La verdad es que me siento obligado a escribir ya que lo he hecho antes en los dos blogs anteriores 🙂
Vamos a ver ¿que te puedo decir? ¿qué está mal? NOOOOOOO
¿Qué es la mejor solución? pues no se que decirte, me parece forzada consecuencia de intentar dar rodeos, de evitar una técnica que te quitaría mucho código.
La solución está muy bien pero muy DRY no es, se pueden ver muchas veces los métodos insertar e insertarVarios reimplementados una y otra vez con pequeñas variantes (adiciones), esas variantes o adiciones son las que invitan a plantearse que a lo mejor un poco de herencia no mata a nadie, la sobrecarga de un método es precisamente para eso, para evitar repetir una y otra vez el mismo código y añadir un poquito más de código (en poquitos casos se redefine completamente). Si añadimos más métodos a Lista la cosa se complica exponencialmente como tú mismo bien intuyes y lo achacas al «mal diseño de Java», por otra parte las clases anexas compuestas son extremadamente simples (un método), si renuncias a la herencia en esas clases cuando se complican podrás llegar a una laaarga cadena de composición que se reduciría muchísimo si no se renunciara a la herencia.
Yo jamás te diría que la alternativa SIEMPRE es un mega-árbol de herencia y nada de composición, sería estúpido por mi parte, es simplemente pensar que no hace daño.
La situación es que necesitas tener una clase Lista, también variantes (dos al menos) que tengan algún tipo de filtrado y todas ellas se puedan auditar.
Me salen en plan «guarro» (explicaré luego porqué guarro)
ListaSinEspacios -> ListaFiltrada -> ListaAuditable -> Lista
ListaConLongitudPar -> … (idem anterior)
ListaSinEspacios y ListaConLongitudPar apenas tendrían un método definido en la clase abstracta ListaFiltrada:
public boolean esInsertable(String elemento)
El método insertarVarios llamaría a insertar(String), la máxima reutilización casi siempre es beneficiosa y lo contrario casi siempre da problemas como el obligar a redefinir dos métodos en vez de uno.
El resultado es la mitad de código y las factorías mucho más sencillas. Permitiría derivar fácilmente también de Lista, de ListaAuditable y de ListaFiltrada para otros tipos de Lista con el mínimo código.
Ahora bien la solución es extremadamente sencilla pero un poco guarra como cualquiera puede intuir, alguien seguramente querrá ListaFiltrada sin auditabilidad, pero en este caso no es problema porque siempre se podrá configurar la auditabilidad por composición para explicar «como se audita», por lo que en este ejemplo no encuentro una razón muy clara para evitar una herencia de ListaAuditable, pero habrá casos en donde se querrá un ListaSinEspaciosTipoA y ListaSinEspaciosTipoB y TipoA tendrá poco que ver con TipoB, en donde una herencia de ListaTipoA y ListaTipoB ciertamente daría lugar a una explosión de combinaciones y en donde la herencia perdería su potencia porque el código de ListaSinEspaciosTipoA tendría que reutilizarse por composición con ListaSinEspaciosTipoB al estar en ramas diferentes. Es en ese caso en donde podríamos prescindir de heredar de forma forzada de ListaTipoA y ListaTipoB y meter estos dos (o uno u otro) por composición que sería el equivalente de dejar de heredar de ListaAuditable (aunque en este caso no he encontrado una razón clara).
Como puedes ver yo empiezo pensando en herencia (si tiene sentido) la cual me permite gran simplicidad, mínimo código y máxima reutilización pero con cuidadito y componiendo cuando sea necesario y cuando la cosa se complica y por tanto refactorizando cuando sea necesario, a veces quitando herencias ¿por qué no?.
El que haya algo de violación de DRY (si te estoy entendiendo bien) tiene que ver con JAVA. Es un tema de lenguaje. En JavaScript o Ruby no haría falta. Por otra parte hacer lista.insertar no se distingue mucho de super.insertar, en cuanto a duplicación de código se refiere. Aunque desde el punto de vista de la encapsulación sí que son muy diferentes.
Ok, ¡ lógicamente no creo que uses siempre herencia !
Me gusta la solucion final!
De este post quitaria las dos lineas de disclaimer de arriba. Ya que lo escribes, asegurate de que esta bien escrito y no escurras el bulto diciendo que lo escribiste tarde. Eso le quita seguridad al post.
Hecho en falta que enlaces a los dos posts anteriores y que en los anteriores enlaces a los siguientes.
Pienso incluir estos posts en mis formaciones y me ayudara que se pueda navegar de unos a otros 🙂
Queremos mas!
Ahí va un artículo en la misma línea e incluso con un título parecido: ‘why extends is evil’: http://www.javaworld.com/javaworld/jw-08-2003/jw-0801-toolbox.html?page=1
flakeyfoont yo me quedo más con la respuesta de Cedric Beust a ese artículo:
http://beust.com/weblog2/archives/000004.html
«This article, where Allen Holub is unconvincingly trying to make a case that inheritance is evil, is no exception.»
«Instead of giving in to simple sensationalism, how about studying the pros and cons of inheritance and trying to educate your readers objectively?»
Me ha gustado mucho la serie de artículos, estoy totalmente de acuerdo en tener mucho ojo con la herencia.
Sólo como comentario podríamos decir que extender de una clase abstracta es algo parecido a tener a módulos de funcionalidad para compartir entre varias implementaciones, pero mal usadas tienen el mismo peligro que extender de una implementación concreta.
La composición mola sobretodo cuando en vez de tener un método de delegación directa (en este caso describirContenido), tienes 20 o 30…
Y si, es culpa de que Java no tenga un mecanismo de delegación, pero eso me suena un poco contradictorio con decir que «extends es una mierda». Si extends… ¡es de Java :-D!
Aun así, el ejemplo me parece muy instructivo para los casos en los que se puede aplicar sin sufrir lo que comento arriba, pero en el caso de Java no me parece definitivo para dejar de usar herencia…. no silver bullet :-).
PD: en realidad, Java si tiene un mecanismo que permite hacer delegación sin sufrir en exceso, usando un «dynamic proxy» sobre el objeto contra el que delegas (aunque requiere que dicho objeto implemente una interfaz con todos los métodos contra los que quieres delegar). Conocí esa ténica hace poco, y me sorprendió lo poco extendida que está.
Sí, con el dynamic proxy se puede hacer un framework/librería para delegación. Yo creo que no se usa mucho por desconocimiento, y porque hacer un proxy dinámico es algo complejo, que potencialmente puede dañar la legibilidad del código y ser bastante costoso. Pero ya hay algunos frameworks JAVA que te permiten hacer delegación de forma muy sencilla y clara. Echa un vistazo a Qi4j, donde puedes componer clases de modelo (entidades) con role interfaces de forma indolora y en tiempo de ejecución.
Sobre extends y JAVA, yo lo que critico es el mecanismo de herencia de implementación, que está también presente en otros lenguajes, no solo en JAVA, como por ejemplo Ruby y JavaScript (herencia prototípica).
Hola!, esta de sobra decir que las entradas son geniales y muy bien redactadas.
Solo que me quede con una duda. Supongamos que tenemos una clase Node y una clase Graphics, y queremos crear una clase que tenga las funcionalidades de Node y Graphics osea un GraphicsNode. Para este caso como se podría eliminar la herencia múltiple?
No deberías tener una clase que sea a la vez Graphics y Node. Me da la impresión que estás mezclando responsabilidades en la misma clase, violando el SRP. Yo sospecharía de este diseño (¿tal vez estás mezclando el modelo con la vista?). Pero sin darte la brasa, y suponiendo que realmente ese diseño está justificado, se puede hacer de la siguiente forma.
Asumiendo que lo haces en JAVA, lo primero es tener dos interfaces Node y Graphics. Después creas una clase GraphicsNode que implemente ambas interfaces. Esta classe tiene un constructor que recibe dos parámetros: una implementación de Node y otra de Graphics. La idea es que la implementación de los métodos simplemente deleguen en las instancias que te hayan pasado por el constructor. A partir de ahi en los métodos de GraphicsNode puedes añadir lógica extra, que sería lo equivalente a «sobreescribir» los métodos en un diseño basado en herencia de implementación.
Si lo que pretendes es realmente coordinar a Node y Graphics, podemos volvier al tema del buen diseño. Si Node sea un modelo, y Graphics una vista, es probable que en GraphicsNode quieras introducir lógica de coordinación entre la instancia de Graphics y la de Node, como repintar cada vez que Node cambie, o actualizar Node cada vez que el usuario haga algo. Desde este punto de vista GraphicsNode se empieza a parecer sospechosamente a un controlador…, y acabarías de implementar un MVC como el que no quiera la cosa. Tal vez con una herencia múltiple te hubiera costado más trabajo llegar a un diseño MVC.
Bueno, antes que nada dejar en claro que no soy muy conocedor sobre estos temas, ni mucho menos tengo experiencia. Lo poquito que he aprendido se lo debo a blogs como este 😀
No espero que me resuelvas el problema, solo espero y me puedas ayudar. Mira el siguiente diseño tiene infinidad de errores que no se como corregir. Por ejemplo, se muestra un «Diamante de la muerte» que se puede dar a futuro, cosa que quiero evitar. También, que pasa si quiero añadir un «Connector» que conecte tres «Nodos», tendría que adicionar otra clase «ConnectorThree» y a parte reimplementar lo gráfico.
Diagrama de clases: http://dl.dropbox.com/u/21063554/Clases-I.png
Estoy utilizando el Framework Qt con C++ en donde se permite la herencia múltiple, hasta el mismísimo Qt la utiliza (http://qt-project.org/forums/viewthread/18129/).
Con respecto al SRP, yo creo que esta bien así, una clase se encarga de realizar las «conexiones» y otra de dibujar, solo que la de dibujar hereda de la de «conexiones».
Está claro que necesitas implementar alguna variante del MVC. Según veo en tu diagrama no tienes capa controladora separada, sino que la lógica de sincronización entre la vista y el modelo está mezclada en las clases Graphic*.
Algunas variantes interesantes de MVC son PassiveView (http://martinfowler.com/eaaDev/PassiveScreen.html) y PresentationModel (http://martinfowler.com/eaaDev/PresentationModel.html). Lee esos enlaces que son muy interesantes. En el mismo site hay más artículos sobre MVC.
Hola Enrique, ¿que te puedo decir?….. pues que muchas gracias por el tiempo que te has tomado en escribir esta serie de artículos.
He aprendido mucho leyéndolos y de ahora en adelante estaré atento a tu blog, que seguro que sigues sacando más artículos como este.
Sobre las ventajas que planteas acerca de la conveniencia de la Composición y Delegación sobre la herencia de implementación, debido a que no rompes la encapsulación, es dinámico (de ahí el uso de Strategy) en contra de la herencia de implementación que es estático debido a que se genera en tiempo de compilación etc….. ¿ Como ves la estrategia de herencia pero a través de clases abstractas ?
Una cosa más me voy a comparr el libro de GoF de Erich Gamma que nombras en tus posts, es un clásico de la ingeniería de software, pero al estar esctito hace tanto tiempo, consideras que hay apatrtados del libro que quizá deberían de ser revisados??? lo digo porque esta escrito en la año 1994.
Un saludo y una vez más gracias.
Para mi el GoF sigue conservando la magia. Eso sí, los ejemplos de código están en lenguajes «antiguos». Quizás modernizar los ejemplos de código, y comentar sobre la conexión entre los patrones de diseño OO y la programación funcional serían aspectos a añadir.
Yo sólo veo bien la herencia de tipos, lo que en JAVA se llama «implements». O sea heredar entre interfaces o una clase de una interfaz. Las clases que tienen todos sus métodos abstractos son lo mismo que las interfaces.
Lo importante es que la herencia modele una relación «el tipo X es un caso especial del tipo Y», es decir, para modelar especialización. Pero no es adecuada para reutilizar código de forma general, ni para modelar composición de funcionalidad ni para añadir funcionalidad (o sea «extend»er). Para esto último composición y delegación.
Hola Enrique, me gusta mucho tu blog, estoy aprendiendo un montón, como ya te he comentado en el post anterior, concretamente la serie de extends me han parecido geniales.
Tengo que reconocer que el último me ha costado un poco entenderlo, aunque una vez entiendes el catalogo de patrones usados ya no tanto.
Me gustaría preguntarte, si te ves con ganas y ánimo hacer una serie de hacer un artículo de este tipo que se centre en el refactoring de código cumpliendo los 5 principios S.O.L.I.D.
Ya he visto que has publicado uno sobre Single Responsability, quizá este sea el más importante junto al de Abierto a extensiones, Cerrado a Modificaciones (O.C.P) ¿ verdad?
Aunque bien es cierto que el principio de OCP lo ilustras estupendamente en este último ejemplo, haciendo uso de la composición de objetos , sobre todo usando Decorator y Strategy y no en la herencia de implementacion.
Un saludo y muchas gracias
Hola Vicente, muchas gracias. Lo de hacer más posts sobre SOLID lo tengo que pensar. Necesitaría algún ejemplo de código que no fuera muy abstracto, y a la vez, que no estuviera muy repetido. Tengo que darle una vuelta.
Salud !
[…] https://eamodeorubio.wordpress.com/2012/02/20/extends-es-una-mierda-composicion-y-delegacion/ […]