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.
Read Full Post »