Hola de nuevo, como vimos en el anterior post la herencia de implementación puede ser un verdadero problema. Sí, es verdad, existen técnicas para diseñar nuestras clases para que puedan ser reutilizadas mediante herencia. Notad la cosa tan fea que acabo de decir. Si quiero que mi clase sea reutilizada, tengo que diseñarla de antemano, es decir, no importa para nada la interfaz de la clase, ni los tests que tenga sobre esta, ni la especificación del contrato. Si quiero reutilizar utilizando «extends» tengo que saber como está implementada la clase internamente, incluyendo todas las cosas «private» que tenga por dentro. A algunos les parecerá de lo más normal, a mi me da que pensar, ¿y entonces, para que leches sirve la encapsulación?
En cualquier caso voy a profundizar un poco en como hacer las cosas «bien» con la herencia de implementación, para ver las limitaciones de la técnica, y para ver si me quito el San Benito de programador petardo.
Tras reflexionar sobre el problema de mantenibilidad que tienen con la clase «Lista», el equipo decide contratar a un consultor externo, y tras convencer a la mesa de compras de que paguen la estratosférica tarifa de 45 euros/hora, consiguen contratar a uno de los más reputados expertos locales. Haciendo honor a sus honorarios, el experto diagnostica y resuelve el problema en un santiamén (lástima que cobre jornada o fracción). «Pero señor@s», dice el experto, «¿es que nunca habéis leído el GoF?», continua, «aquí os pongo un patrón Template Method que os vais a chupar los dedos». Ni corto ni perezoso modifica el código, y ListaSencilla le queda así.
public class ListaSencilla implements Lista { private List datos = new ArrayList(); public ListaSencilla() { super(); } @Override final public void insertar(String elemento) { datos.add(elemento); procesamientoAdicionalTrasInsertarElemento(elemento); } @Override final public void insertarVarios(Collection elementos) { for (String elemento : elementos) insertar(elemento); } /** * 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, es un "hook" para sobreescribir } @Override public String describirContenido() { StringBuilder descripcion = new StringBuilder("Contenidos: "); for (String elemento : datos) { descripcion.append("'"); descripcion.append(elemento); descripcion.append("' "); } return descripcion.toString(); } }
Nótese el uso de la palabra clave «final». Esta palabra clave se puede poner a nivel de método, o mejor, a nivel de clase, indicando que no se puede sobrescribir el método o hacer extends de la clase. La palabra clave «final» es un gran invento de JAVA, pero claro, hay que leer entre líneas: un lenguaje que tiene herencia de implementación, pero que además tiene una palabra clave que prohíbe la herencia, muy sospechoso. En realidad los diseñadores de JAVA sabían muy bien que la reutilización de código mediante herencia de implementación es peligrosa. Conscientes de ellos, añadieron «final», para dar la capacidad al desarrollador de marcar una clase o método con «¡Peligro, no heredar o sobrescribir!», en el caso de que no hubieran diseñado de antemano la clase para herencia. Desgraciadamente no he visto que mucha gente use «final».
Siguiendo con la historia, al usar «final» y «Template Method» en conjunción con el método protegido «procesamientoAdicionalTrasInsertarElemento», se desactiva cualquier problema. Por un lado no podemos sobrescribir los métodos peligrosos, ya que son «final». Por otro lado el implementador de la clase diseña su código de tal forma que si quiero reutilizar la clase, no lo pueda hacer de cualquier forma, sino sólo sobrescribiendo el método protegido a tal fin. Con este cambio, la clase «ListaAuditable» queda como sigue:
public class ListaAuditable extends ListaSencilla { private Auditor auditor; public ListaAuditable(Auditor auditor) { super(); this.auditor = auditor; } @Override protected void procesamientoAdicionalTrasInsertarElemento(String elemento) { auditor.elementoInsertado(elemento); } }
Da la impresión de ser un código bastante elegante. Desde luego ha merecido la pena pagar la exorbitante tarifa del consultor externo. Sólo hay un problema pequeñito, el test de «ListaSencilla» se ha complicado. Claro, ahora no sólo hay que testear el contrato «Lista» en «ListaSencilla», que es el que define como se usa la clase por parte de los consumidores de esta. Ahora hay otro contrato, representado por el método protegido, entre «ListaSencilla» y todas las clases que la extiendan. Este contrato es «en negro», ya que no es público, sino «protegido». Necesitamos un test que pruebe que el método protegido es llamado en los momentos adecuados y con los parámetros adecuados. Pero este contrato no está en una interfaz de un colaborador, sino en un método «protected», ¿cómo hacemos el test? Afortunadamente con un poco de programación «clever» se puede terminar haciendo testing de esto. Lo dejo como ejercicio al lector.
Otro pequeñito problema es que alguien siga pensando que no tiene por que entender la implementación de la superclase, que le vale sólo entendiendo el contrato público (pobrecito), y al heredar haga cosas como esta:
public class ListaAuditablePetarda extends ListaSencilla { private Auditor auditor; public ListaAuditablePetarda(Auditor auditor) { super(); this.auditor = auditor; } @Override protected void procesamientoAdicionalTrasInsertarElemento(String elemento) { super.insertar(elemento); // Ouch !!! auditor.elementoInsertado(elemento); } }
¡ Ay, que cosa más fea ! ¿Es que nadie se lee el Javadoc? Obviamente nadie debería usar super para llamar a un método peligroso (final), sólo sobrescribir métodos protegidos o métodos públicos no peligrosos (no final). Bueno, a parte de estos problemitas subsanables con un poco de «hacking», y despedir al inútil que cometió la anterior tropelía, parece que la técnica funciona.
Pasa el tiempo y aparece otro problema con este enfoque. ¿Qué ocurre si queremos hacer la extensión de una clase de una forma no prevista de antemano? No puedes. Tienes que volver a modificar la clase padre para añadir extensibilidad en el nuevo «eje» que te haga falta. Por supuesto en cualquier desarrollo serio, ha habido una extensa e intensiva fase de análisis funcional y diseño técnico, y esas cosas no pueden pasar, ya que se han cubierto todos los posibles cambios. Desgraciadamente nuestros amigos no son tan profesionales y usan una cosa llamada «agile» que les impide hacer un buen análisis y diseño «up-front» como dios manda, y no tienen muy claro las necesidades de diseño futuro de su sistema.
Nuestros sufridos desarrolladores se encuentran con un nuevo requisito, ahora resulta que algunas especializaciones de «ListaSencilla» pueden rechazar un elemento, y negarse a insertarlo si cumple alguna característica concreta. Desgraciadamente en las clases hijas de «ListaSencilla» no pueden añadir esta funcionalidad, deben abrir su implementación, y siguiendo «Template Method» añadir un punto de extensión más.
public class ListaSencilla implements Lista { private List datos = new ArrayList(); public ListaSencilla() { super(); } @Override final public void insertar(String elemento) { if (!esInsertable(elemento)) throw new ElementoRechazadoError(elemento); datos.add(elemento); procesamientoAdicionalTrasInsertarElemento(elemento); } @Override final public void insertarVarios(Collection elementos) { for (String elemento : elementos) { if (!esInsertable(elemento)) throw new ElementoRechazadoError(elemento); } for (String elemento : elementos) insertar(elemento); } /** * Este método es invocado para pedir permiso sobre si un elemento * puede ser insertado o no * * @param elemento * El elemento que queremos insertar * @return si se puede insertar o no */ protected boolean esInsertable(String elemento) { return true; } /** * 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(); } }
Bueno, no duele tanto, al fin y al cabo el código parece razonablemente limpio y «ListaAuditable» ni se ha enterado. Ahora implementamos el requisito, realmente ocurre que pueden existir tres tipos de listas auditables, aquellas que no admiten elementos que contengan espacios y aquellas que sólo admiten elementos de longitud par. Bueno, a implementar. La cosa queda como sigue:
public class ListaAuditableConElementosSinEspacios extends ListaAuditable { public ListaAuditableConElementosSinEspacios(Auditor auditor) { super(auditor); } @Override protected boolean esInsertable(String elemento) { return !elemento.contains(" "); } }
Y también:
public class ListaAuditableConElementosLongitudPar extends ListaAuditable { public ListaAuditableConElementosLongitudPar(Auditor auditor) { super(auditor); } @Override protected boolean esInsertable(String elemento) { return elemento.length() % 2 == 0; } }
Ok, no esta nada mal, casi podemos cantar victoria, ¡además hemos reutilizado «ListaAuditable»!
Pasa el tiempo, y con él llegan cambios, e inevitablemente nos llega otro requisito. Resulta que las listas que no son auditables también pueden existir en la forma de listas que no admiten elementos con espacios, y las que sólo admiten elementos de longitud par ¡Qué dura es la vida del desarrollador! De nuevo al código, queda lo que sigue:
public class ListaConElementosSinEspacios extends ListaSencilla { public ListaConElementosSinEspacios() { super(); } @Override protected boolean esInsertable(String elemento) { return !elemento.contains(" "); } }
Y también:
public class ListaConElementosLongitudPar extends ListaSencilla { public ListaConElementosLongitudPar() { super(); } @Override protected boolean esInsertable(String elemento) { return elemento.length() % 2 == 0; } }
¡Han aparecido otras dos clases! ¡Ya tenemos 6 implementaciones de «Lista»! Bueno, al menos son cortitas y sencillas, y estamos reutilizando un montón. El problema es que estamos a empezar a detectar un tufillo a violación del DRY. De hecho es la misma implementación de antes pero heredando de «ListaSencilla» en vez de «ListaAuditable» ¿Quién dijo que el copy&paste era malo? Quizás si en vez de usar «extends» usáramos otra cosa… nada, todo el mundo sabe que la herencia es «esencial» a la OO, su «killer feature», mejor seguimos con el «Template Method», y no liamos más la cosa.
Seis meses más tarde tenemos 60 clases «Lista». Resultó que salieron cinco formas más de filtrar los elementos de las listas, y encima además de listas «auditables» y «sencillas», tenemos listas «lazy», listas «persistentes», etc. En fin, que necesitamos una clase por cada combinación posible. La verdad es que esto ya no parece tan limpio.
Una última reflexión. Para el que sea propenso a pensar en cosas inútiles, aquí lanzo una pregunta, ¿cómo es mejor implementar ListaAuditableConElementosLongitudPar? ¿Heredando de ListaConElementosLongitudPar o heredando de ListaAuditable? ¿Da igual? Y en caso de que sea igual, ¿no parece un accidente histórico el decidir implementarla de una forma y no de otra, y no una decisión de diseño guiada por criterios de ingeniería del software? Inquietante, al menos para mi.
Reconozco que este es un ejemplo un poco «cogido por pinzas», pero no me negaréis que ilustra un problema real, que tal vez muchos estáis reconociendo de haberlo sufrido. Esta explosión combinatoria de clases es algo que se conoce también desde antiguo, y está ligado al hecho de que tenemos herencia de implementación simple. Si alguno está pensando en solucionarlo con herencia de implementación múltiple, nada vosotros mismos. Si no habéis tenido bastante con la herencia simple, ahora si que os podéis meter en un buen lío. Está claro que el verdadero problema reside en usar «extends» como mecanismo de reutilización de código.
Resumiendo:
- Puedo reutilizar código con «extends», pero tengo que diseñar de antemano para ello.
- Al diseñar de antemano, sólo puedo reutilizar aquello que está pensado para serlo, y no cualquier característica pública de la interfaz de la clase. Si necesito reutilizar mediante «extends» alguna característica de la clase que no esté diseñada para ello, pues o me aguanto, o no uso «extends» o simplemente abro la clase padre y la modifico (muy SOLID no es, no).
- El que reutilice con «extends» una clase debe conocer si ésta está diseñada para ello, y por lo tanto conocer la implementación interna de la clase padre.
- La forma más común (no la única), es usar un patrón «Template Method» de GoF, pero esto dificulta el testing, ¿cómo pruebo los métodos protected?
- Algún programador petardo, que no se haya leído nuestro excelente Javadoc de los métodos «protected», puede acabar invocando al «super» y terminar con errores en las nuevas clases, errores que a veces son sutiles de detectar.
- Esta forma de reutilizar código está sujeta además a la explosión combinatoria de clases.
Algunos argumentaron en el post anterior que «extends» no sirve como mecanismo de reutilización de código, sino como mecanismo de especialización. O sea, para especializar clases pero sin reutilizar código. Pues vaya tela, ¡yo pensaba que lo que mantenían relaciones de especialización eran las interfaces!. Y que alguien me diga, ¡ para que sirve la herencia de implementación si no es para reutilizar código ! ¡Para que quiero especializar una clase sin reutilizar su código! ¡ Quiero un ejemplo concreto !
¿Existe alguna forma mejor de reutilizar código? La respuesta es sí, pero viene con un precio a pagar. A algunos, como a mi, este precio le parece pequeño, pero a otros les parece enorme. Lo veremos en el siguiente post.
esto parece un capitulo del batman de los 70… primero nos dejas con el culo torcido y luego no das la solucion hasta el proximo capitulo…
como no vayas del palo «delegate» no se me ocurre nada….
Dandole intriga, eh? muy bueno el post tio 🙂
Una solucion que se me ocurre para esto es el patron Decorator. Tiene pinta de que encajaria bastante bien. Ya leere el siguiente post a ver que tal lo planteas 😉
Enrique en el anterior post hablamos de abuso y tu mismo estas poniendo un ejemplo de abuso o de mal uso si quieres, para negar el todo, en ese abuso estas combinando varios conceptos ortogonales en el mismo arbol de derivacion, de verdad todo esto es muy antiguo, no estamos en los ochenta cuando se popularizo el invento y el entusiasmo cegaba, no te voy mencionar la herencia multiple pues aparte de excesiva casi siempre , ni siquiera existe en muchos lenguajes, pero cualquiera con un poco de experiencia parte tu arbol en varios arboles vinculados por agregacion y si acaso los especia con herencia multiple de interfaces, reconoceria que la auditabilidad merece la pena ser controlable desde la propia clase base a traves de un objeto diferente que a su vez podria tener herencia para modelar como se audita, como ves al final es el arte de la componer y heredar, componer y heredar
Se que vas hacia una solucion por composicion sin herencia de implementacion y es tu opcion vale, pero recomendarlo como LA opcion es terrible, es como proponer la cojera o rechazar los antibioticos porque producen molestias intestinales
Por fortuna buena parte de los mejores programadores los que hacen las herramientas que mueven el mundo no siguen estas ideas, lo veo en sus javadoc en su codigo fuente, sino seria de locos Ki
Ciertamente estoy abusando de «extends». Un punto importante de lo que cuento es mostrar que se puede abusar muy fácilmente de «extends», porque es muy frágil, y por lo tanto peligroso. No es el caso de técnicas como composición y delegación como intentaré mostrar en el siguiente post.
A mi lo que me gustaría ver es un ejemplo claro y sencillo donde «extends» sea una buena elección de diseño, y no un abuso, y que el software resultante sea mantenible a largo plazo. Fíjate que en el anterior post la funcionalidad era muy simple, y aun así nos metimos en un lío. En este, para arreglarlo aplicamos «Template Method», y todo parecía ir bien, pero poco a poco, e incrementalmente, la cosa ha terminado mal. Yo creo que este ejemplo es bastante sencillo, y los requisitos y cambios que van apareciendo no son nada del otro mundo.
Yo he programado a nivel profesional plugins para eclipse, y te puedo decir que en la API de eclipse, en los puntos donde me he encontrado «extends», he sufrido de lo lindo. No os podeis ni imaginar lo frágil que es extender una herramienta, cuando los puntos de extensión te obligan a hacer «extends». Desconozco si ahora han arreglado la API y quitado el extends.
Lo que comentas es muy parecido a lo que pone Josh Bloch en Effective Java en item 16: «favor composition over inheritance». y pone un ejemplo muy parecido al de auditar tuyo 🙂
como también se ha dicho aquí se recomienda herencia sólo cuando de verdad A es un tipo de B, no simplemente por reusar codigo.
Muy interesante también lo que dice en el item 17: «design and document for inheritance or else prohibit it», en el que entre otras cosas dice «la clase debe documentar el uso propio de métodos que sean sobreescribibles (no finales y públicos o protected)», en el ejempo que ponías se debería documentar que insertarVarios llama a insertar por cada elemento.
También está el conocido problema de que desde un constructor no puedes llamar a métodos públicos si no quieres que en las clases hijas pasen cosas raras (que se llame a un método sobrescrito en el hijo antes de estar inicializado el objeto).
Vamos, que estoy de acuerdo que en general es mejor usar composición, pero la herencia también tiene sus usos 🙂
Leches, os habéis leído todos el «favor composition over inheritance» y yo no.
Yo cuando quiero modelar la relación «X es un caso especial de Y», hago herencia de tipos, es decir, «extends» entre interfaces e «implements» entre interface y clase. Incluso el «extends» entre interfaces no se puede hacer alegremente, y hay que asegurarse que no se vaya a violar el principio de segregación de interfaces. En general en un sistema diseñado mediante «role» interfaces no suele ser necesario hacer herencia entre interfaces.
Lo que comentas de la documentación me pone los pelos de punta. Si documento que «insertarVarios» llama a «insertar», entonces estoy documentando la implementación. Es decir, estoy repitiendo la implementación dentro del JavaDoc. O sea violar el DRY y el principio de encapsulación. Para eso, ya que estamos, no hagamos JavaDoc, y que se miren el código fuente. Al menos me ahorraré duplicación y violación de DRY (pero seguiré violando la encapsulación).
¿Violar la encapsulación? Sí. Si cambio la implementación de «insertarVarios», ahora tengo que cambiar su JavaDoc para decir que ya no llama a insertar, y por lo tanto me he cargado a todos los clientes que esperaban que «insertarVarios» llamase a «insertar». Vamos, que en un JavaDoc no debe haber ninguna referencia a la implementación interna de un método, es más, un consumidor de la clase no debe depender de detalles internos de implementación. O sea, lo que se llama encapsulación de toda la vida.
Ya muchos me decís lo mismo: «a veces la herencia de implementación tiene sus usos». ¿Cuál uso aparte de reutilizar código? Nadie me ha mandado hasta el momento un caso de uso de la herencia de implementación cuya intención no fuera reutilizar código.
Por partes como Jack el Destripador:
Imprescindible el libro de Effective Java, no sólo por profundizar en Java sino también en conocimiento OO.
Por definición, la herencia rompe el encapsulamiento (da igual que sea Java, Ruby o el lenguaje que quieras), la clase hija va a estar muy acoplada a la padre, si vas a extender de una clase necesitas tener un conocimiento mucho más íntimo de dicha clase que si simplemente la usas. Por tanto deberías documentar muchas más cosas en una clase en la que quieres dejar que hereden de ella (en el libro te dice todo lo que deberías documentar). Entre otras cosas, necesitas saber a que métodos públicos se está llamando en el padre desde otros métodos (para que no te pase lo del ejemplo), que si no lo pones en el Javadoc, al menos deberías poder ver el código fuente. De hecho yo nunca herederaría (o lo haría con extremada precaución) de una clase de la que no tenga el código fuente, porque normalmente no se documenta todo lo que realmente necesitarías saber de dicha clase (que insisito, es mucho más de lo que necesitas saber si simplemente la vas a usar).
Obviamente siempre que se usa herencia se reutiliza código (si no, usarías sólo interfaces), pero lo que me refiero es que no tiene por qué ser el objetivo principal. El objetivo principal es modelar una relación de «es un», y obviamente eso implica que pueden tener comportamientos comunes que puedes reaprovechar. Por ejemplo tener un clase Contacto y una Cliente que añada funcionalidad sobre Contacto podría ser un caso válido de herencia.
Relacionado (aunque puede que no mucho), una cosa que me chocó al principio pero lo explica muy bien en el libro es que es imposible heredar de una clase (no abstracta) añadiendo algún atributo que contribuya a la identidad y respetar el contrato del «equals», no podrás mantener a la vez las propiedades reflexiva, transitiva y el principio de sustitución de Liskov (es un problema de principios de la herencia al implementar relaciones de equivalencia, da igual el lenguaje, no es problema de Java).
Enrique te voy a dar parcialmente la razón, yo tengo a mis espaldas varias librerías de dominio público (ItsNat, JNIEasy, JEPLayer, LAMEOnJ…) y la verdad es que prácticamente nunca (salvo algún caso anecdótico) expongo una clase para que herede, es más es que no hay clases públicas salvo un par de clases anecdóticas y una de ellas siempre es la arrancadora de la librería.
¿No es contradictorio?
No, dentro de todas esas librerías uso herencia de implementación A SACO.
¿Cual es la diferencia?
Pues la diferencia entre dentro y fuera, público y privado. No expongo clases por puro egoismo, porque no quiero que me quiten libertad de implementación de mis clases.
Como decía magistralmente jneira, la herencia es muy poderosa pero requiere un equipo cohexionado, como eso no es posible en el caso de terceros no los invito a heredar. En el código interno se hace y deshace lo que sea, si una clase base cambia pues se revisan TODAS las clases derivadas si es necesario, cualquier IDE (y en eso NetBeans es el mejor que conozco) te ayuda a ver quien hereda, quien sobrecarga un método etc. Esto no lo puedes hacer cuando hay código de terceros.
Ahora bien renunciar a la herencia como extensión tiene un precio MUY alto, tienes que compensar en ofrecer listeners aquí y allá, formas de que te digan que esto no ha de llamarse etc etc todo a base de registros y objetos agregados del usuario, obligándoles a implementar montones de métodos para los cuales tienes una funcionalidad ya por defecto en tus clases internas pero que no les puedes ofrecer o si lo haces es a base de agregación torpe en plan decorator.
No hay más que ver el ejemplo de clase MyCustomComponentBase llena de métodos que normalmente no se necesitan pero que hay que implementar:
http://www.innowhere.com:8080/itsnat/feashow_servlet?itsnat_doc_name=feashow.main&feature=feashow.comp.other.customComponent.code
Y esto en mi caso es posible porque son cosas relativamente sencillas, ahora bien, si le dices a los autores de estos:
http://docs.oracle.com/javase/1.4.2/docs/api/javax/swing/JCheckBox.html
http://docs.oracle.com/javafx/2.0/api/javafx/scene/control/RadioButton.html
que rehagan ese árbol de derivación como agregaciones y que ofrezcan la posibilidad de extender ambos para terceros por una vía que no sea herencia de implementación, te dirán que…. y una mierda (por utilizar el título del blog) 🙂
gato escaldado del agua fria huye
Enrique sientete libre de publicar la tercera entrega de tu serie, no todo el mundo tiene que pensar lo mismo, yo en casi todo suelo estar en minoría por lo que es bueno que no te den siempre la razón 🙂
Es más tus dos artículos aunque no estoy de acuerdo con el objetivo de los mismos, han sido MUY educativos.
Saludos 😉
Bueno, yo prefiero qué haya opiniones plurales en este blog. Parte de mi estilo incendiario tiene como objetivo promover el debate, y qué la gente defienda sus ideas. Vamos qué me gustan los debates sanos, e incluso un poco conflictivos.
[…] Comentarios « ¡Extends es una mierda! Diseñando para la herencia […]
Hola la verdad me impacto el hecho de que haya mas gente con el concepto apropiado de la herencia, la herencia de implementacion te hace ahorrar muchas lineas de codigo y todo el mundo cree que correcto por que todo el mundo lo usa.
Yo vengo de C++ y esa palabrita no existe aunque no hace falta que este para hacer una cosa u otra… la palabra extends como extensión esta mal pensada, muchos creen que extendemos y lo hacemos bien cuando hablamos de extension pero en realidad la extension es cuando implementas una interfaz por que en el futuro la extension viene cuando puedes cambiar ese objeto por otro sin alterar el comportamiento interno del contexto que usa el objeto.
Muy buen articulo!!