Effective Java: Debatiendo conceptos

En uno de los hilos de discusión de este Foro, se comentaba sobre algunos libros para adentrarse más en buenos principios de diseño, y surgió el libro de Joshua B. "Effective Java".

Pensé que a lo mejor y resulta útil comentar sobre los Items que se discuten en ese texto y seleccioné para empezar el 16.
Es un principio de diseño muy comentado en la literatura. El libro de GOF también lo discute, y varios APIs del JDK hacen buen uso y también mal uso de él.

¿Quien empieza?

Item 16: Favor composition over inheritance
Inheritance is a powerful way to achieve code reuse, but it is not always the best tool for the job. Used inappropriately, it leads to fragile software. It is safe to use inheritance within a package, where the subclass and the superclass implementations are under the control of the same programmers. It is also safe to use inheritance when extending classes specifically designed and documented for extension (Item 17). Inheriting from ordinary concrete classes across package boundaries, however, is dangerous. As a reminder, this book uses the word “inheritance” to mean implementation inheritance (when one class extends another). The problems discussed in this item do not apply to interface inheritance (when a class implements an interface or where one interface extends another).

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.
Imagen de ezamudio

Herencia "profunda"

La herencia es muy útil cuando se usa bien, el problema es saber usarla bien. En muchas aplicaciones, bibliotecas y frameworks, tener clases abstractas con comportamiento básico que se puede (debe, de hecho, porque son abtractas) extender para tener implementaciones concretas ahorra mucho trabajo cuando hay que hacer varias de esas implementaciones. Pero tener una clase D que hereda de C que hereda de B que hereda de A, es señal de que algo está mal. Como dice Kent Beck, un code smell.

Imagen de benek

Recuerdo un comentario que

Recuerdo un comentario que hizo alguien creo que en la primera o segunda #OpenTalk, la herencia mal utilizada incrementa el acoplamiento de una aplicación.

Un concepto que me gusta un poco más en cuanto a reutilización de código y no te restringe con enlazar todo en jerarquías es el concepto de Traits, aunque no están disponibles en Java ni Groovy, en Scala sí.

Imagen de beto.bateria

polimorfismo antes que herencia.

El verdadero poder de la oop se guarda en en la adecuada distribucion de responsabilidades y en el polimorfismo. Si aplicas correctamente estos concepto en las aplicaciones que desarrollas, tu aplicacion tendra las siguientes caracteristicas:
- facil de crecer
- se reducira el tiempo de mantenimiento o correccion de errores
- facilidad de que la entiendan los nuevos programadores.
- y podras crear librerias.
Entre otras cosas.

Les recomiendo que estudien los patrones grasp, ayudan mucho, sobre todo Alta cohesión y Bajo acoplamiento :)

Sucede que la peor forma de

Sucede que la peor forma de acoplamiento ( o la más fuerte pues ) es la herencia, no solo mal utilizada, la herencia bien utilizada también crea un alto acoplamiento.

Cuando se hereda de una clase, la nueva subclase depende totalmente de la clase base. En OO se desea bajo acoplamiento.

Eso no significa que no se deba de heredar, pues como lo menciona el libro ( y el extracto que escribiste ) cuando está dentro del paquete y/o cuando se diseña para ser heredado ( como en las clases abstractas que menciona Enrique ) el precio a pagar por el acoplamiento se compensa con el beneficio de obtener la funcionalidad base.

Pero hay casos en los que no se debe de heredar, solo por "reutilizar el código". De hecho pensar que la herencia sirve para reutilizar es un error, la herencia sirve para especializar el comportamiento y no tanto para reutilizar.

El ejemplo que viene en el libro y el principio para decidir cuando si debe de usar la herencia es cuando existe una relación "is-a" ( es-un ) entre las clases. Cuando puedes decir que todo objeto "B" es-un objeto "A", entonces el acoplamiento aunque sigue siendo alto, es poco importante, porque se está especializando la clase, y sigue siendo de la misma naturaleza. Si no se puede decir con certeza que todo "B" es-un "A" entonces no se debe de heredar, y lo mejor sería, usar "composición".

Los ejemplos que el libro menciona donde se violó este principio en las bibliotecas base mismas de Java por ejemplo fue en Stack heredando de Vector. Ciertamente una pila no es-un vector. O una lista de propiedades no es una tabla hash, por lo que la clase Properties no debió heredar de Hashtable.

Y cito ( chale, no quería llegar de decir "cito", pero por más que le busco lo mejor será explicarlo en sus propias palabras ):

Este es no es un problema meramente teórico. Muchos problemas de seguridad de esta naturaleza tuvieron que ser arreglados cuando Hashtable y Vector fueron adaptados para participar en el "Collections Framework"

A los problemas de seguridad que se refiere, vienen explicados antes. Pero básicamente consisten en mantener un estado interno del objeto que al ser heredado a lo salvaje no puede ser garantizado ( en mis propias palabras ).

Entonces: ¿Como se puede usar la composición en vez de la herencia? Cuando la relación entre dos clases es de "tiene-un" ( has-a ) entonces, estamos hablando de composición. En el ejemplo de Stack ( Pila ) que no es un Vector, podríamos en caso de querer retulizar el código, decir que una Pila tiene un Vector.

Una implementación ( con fines meramente ilustrativos sería ):

 

De esta forma, si se está reutilizando el código, y el acoplamiento, no es tan alto entre clases. Una mejora, para reducir el acoplamiento aún más, sería tan simple como:

 

Pero eso ya es tema de otro post.

El punto clave aquí esta en entender la relación is-a, has-a. Si ya lo quieren ver más formalmente revisar el Principio de sustitución de Liskov ( más extenso y en inglés aquí )

Imagen de bferro

Polimorfismo basado en herencia

Casi todos los lenguajes de programación OO comerciales (los que conocen la mayoría de la gente) basan el comportamiento polimórfico en la herencia. Esto no significa que sea el único medio posible de explotar el polimorfismo en los lenguajes de programación.
Al trabajar de esta forma, no es posible pensar en el "polimorfismo antes que herencia" como lo expresas.

El survey "On the Notion of Inheritance" discute con amplitud conceptos importantes sobre la herencia y otros principios de abstracción importantes del diseño y programación orientada a objetos.
Vale la pena leerlo y confrontar las ideas que ahí se discuten.

Imagen de ezamudio

Polimorfismo

Pero en Java y otros lenguajes podemos hacer polimorfismo simplemente con la implementación de una interfaz. Al implementar Runnable haces polimorfismo porque lo implementas siempre de maneras distintas para hacer algo en un hilo separado o en un pool de hilos, etc. O al implementar ActionListener para responder a un evento de un botón o campo de texto en Swing, etc.

...Esto no significa que sea

...Esto no significa que sea el único medio posible de explotar el polimorfismo en los lenguajes de programación... Ni que el polimorfismo de subtipo ( el conocido en la programación orientada a objetos ) sea el único tipo de polimorfismo que existe, pero si seguimos en ese camino, nos vamos ( una vez más ) a desviar de tema ( como siempre pasa en javamexico ) y vamos a terminar hablando de otras cosas. :)

Imagen de bferro

Y sigue siendo polimorfismo basado en herencia

ezamudio: Pero en Java y otros lenguajes podemos hacer polimorfismo simplemente con la implementación de una interfaz.

Sigue siendo polimorfismo basado en herencia ( en este caso herencia de interfaz, bien de métodos abstractos definidos en una clase abstracta o en una interfaz que se derogan o implementan en los subtipos).

Imagen de beto.bateria

Polimorfismo en otros lenguajes.

Una nota cultural (tiene otro nombre, pero en fin):

En python, debido a que es dinámicamente tipado, no es necesario el polimorfismo y eso me hizo muy feliz para solucionar unos problemitas que tenia en una aplicacion.

Imagen de Nopalin

Polimorfismo

Técnicamente hablando, el polimorfismo como lo dice la wikipedia, es la capacidad para que las subclases utilizen un metodo definido en la clase base de forma distinto. Entonces me pregunto, si la definicion de polimorfismo se basa en la herencia, ¿cómo se puede hacer polimorfismo sin ella?.

La herencia no necesariamente sirve para reutilizar, si no tambien para definir nada más.

Imagen de ezamudio

polimorfismo en python

El polimorfismo es utilizado en lenguajes con tipos dinámicos también. En ObjC yo usaba polimorfismo, no es algo exclusivo de los lenguajes fuertemente tipados. Simplemente es que tengas distintas implementaciones de una clase abstracta, o de una interfaz, o de un protocolo, etc, ya sea formal o informal. En python puedes tener código donde esperas un objeto (de la clase que sea, no importa porque es dinámico) pero la cosa es que ese código invoca el método   del objeto. Un objeto puede implementarlo o no implementarlo; puede que pases distintos objetos que implementan el método de distintas formas. Eso es polimorfismo.

@bato.bateria: Lo que pasa

@bato.bateria:

Lo que pasa es que no es el unico tipo de polimorfismo.

Por ejemplo en la programación funcional, existe el polimorfismo de tipo (en)

Pero... no nos estamos desviando de tema una vez más?

¿Por que no seguimos hablando de preferir composición a herencia?

:)

Imagen de Nopalin

Como dice oscar nos debiamos

Como dice oscar nos debiamos del tema original, pero presisamente de estas desviaciones es donde uno aprende de otras cosas, si todo fuera lineal, no existirian los lenguajes dinámicos, o si?

@ezamudio, la definicion de polimorfismo nos dice que es cuando varias clases sucesoras utilizen un mismo metodo definido en la original de forma distinta. Esto es herencia.

Ahora, que en los lenguajes dinámicos no necesites un tipo para poder llamar a un método y que en tiempo de ejecución te indique si ese objeto lo tiene o no, no deberia ser considerado como polimorfismo ya que en realidad pueden ser objetos completamente distintos que sirven para diferentes cosas, con la peculiaridad de tener un método con la misma firma.

Pero yo hablo de los lenguages orientados a objetos, tal vez la definicion cambie para los lenguajes funcionales.

sobres

Imagen de bferro

Volvamos a componernos y heredarnos

En efecto, el desvío del tema es algo normal debido a que un tema que se discute, tiene relación con otros muy interesantes. Es el caso del polimorfismo cuando se discuten cosas de herencia y composición.

Pero para no desviarnos, mejor comento algunos de los comentarios de los participantes con relación a la composición y herencia.

Herencia y acoplamiento

La herencia tiene poco que ver con el acoplamiento, si por acoplamiento entiendo la interrelación entre objetos. Más bien introduce dos problemas: rompe el encapsulamiento y provoca la fragilidad de la clase base, temas que ya se han discutido en este Foro y para los cuales existen soluciones; entre ellas la composición de objetos.

Herencia y reuso de código

La herencia siempre será útil para ser usada como un mecanismo de reuso de código, siempre que ese uso no eche abajo otras partes del diseño. Es uno de los mecanismos para el reuso de código (reuso de caja blanca) El otro es precisamente la composición (reuso de caja negra).
Por supuesto que hay escenarios donde el reuso de código debe establecer un compromiso con la relación de subtipo que se desea en la jerarquía.
En muchos buenos diseños se utiliza la herencia exclusivamente para reuso de código sin respetar la relación de subtipo (en otras palabras; desconociendo el LSP). Java, a diferencia de C++, del cual tomó muchas cosas, permite que existan clases abstractas con métodos virtuales concretos para precisamente favorecer el reuso de código común en las subclases.

Cuando la relación de subtipo es natural y no conviene

La composición también es útil en casos donde la relación de subtipo puede resultar natural, y no solamente cuando es totalmente evidente una relación de agregación o composición. Algunas instancias del patrón Decorator, como las clases de Streams en Java (InputStream, OutputStream) caen en ese caso, sin embargo, se prefiere un mecanismo de composición para evitar los problemas de diseño rígido, explosión de clases, etc. que conllevaría al usar herencia.

Puede ser natural por ejemplo definir la clase DataFileInputStream como:

 

Para entonces crear un objeto de esa clase como:

 

En lugar de componer esa funcionalidad con dos objetos:

 

Resulta difícil decir que NO es natural la relación de herencia en el primer caso. Pero si ese es el diseño preferido, entonces habría que crear una subclase con la funcionalidad de entrada formateada para cada flujo básico (ByteArrayInputStream, PipedInputStream, etc.) lo que conduce poco a poco a una jerarquía muy grande (léase explosión de clases) que seguirá creciendo al añadir otras funcionalidades. Y lo que es peor, el diseño es estático lo que impide modificar la implementación de esas funcionalidades en ejecución o deshacerla totalmente para usar en su momento al flujo básico.

Imagen de beto.bateria

Varios aspectos.

Primero:
Si se prefiere la composicion sobre herencia.
Respuesta: Dependiendo del sapo es la pedrada. NUNCA hay soluciones universales.

Sobre lenguajes dinámicamente tipado.
Definitivamente no es polimorfismo.

Acerca del polimorfismo (de )

si tenemos:

public abstract class Animal {
public abstract void habla();
}

class Perro extends Animal{
public void habla(){
System.out.println("¡Guau!");
}
}

class Gato extends Animal{
public void habla(){
System.out.println("¡Miau!");
}
}

entonces podemos hacer:

Animal animal = new Perro();
animal.habla();
"¡Guau!"

animal = new Gato();
animal.habla();
"¡Miau!"

sigo en otro post :s