Enlace Patrocinado
He estado programando en lenguajes orientados a objetos durante décadas. El primer lenguaje POO que utilicé fue C ++ y luego Smalltalk y finalmente .NET y Java.
Estaba dispuesto a aprovechar los beneficios de la herencia , la encapsulación y el polimorfismo . Los tres pilares del paradigma.
Estaba ansioso por obtener la promesa de Reutilización y aprovechar la sabiduría adquirida por aquellos que vinieron antes que yo en este paisaje nuevo y emocionante.
No podía contener mi emoción ante la idea de mapear mis objetos del mundo real en sus Clases y esperaba que todo el mundo cayera perfectamente en su lugar.
No podría haber estado más equivocado.
Herencia, el primer pilar en caer
A primera vista, la herencia parece ser el mayor beneficio del paradigma orientado a objetos. Todos los ejemplos simplistas de jerarquías de formas que se muestran como ejemplos para los recién adoctrinados parecen tener sentido lógico.
Y reutilizar es la palabra del día. No … haz que sea el año y tal vez siempre.
Me tragué todo esto y salí corriendo al mundo con mi nueva visión.
Banana Monkey Jungle Problema
Con la religión en mi corazón y problemas que resolver, comencé a construir Jerarquías de Clase y escribir código. Y todo estaba bien con el mundo.
Nunca olvidaré ese día cuando estaba listo para sacar provecho de la promesa de reutilización heredando de una clase existente. Este era el momento que había estado esperando.
Llegó un nuevo proyecto y pensé en esa clase que tanto me gustaba en mi último proyecto.
No hay problema. Reutilizar al rescate. Todo lo que tengo que hacer es simplemente tomar esa Clase del otro proyecto y usarla.
Bueno … en realidad … no solo esa clase. Vamos a necesitar la clase para padres. Pero … Pero eso es todo.
Ugh … Espera … Parece que también vamos a necesitar a los padres de los padres … Y luego … Vamos a necesitar TODOS los padres. Está bien … Está bien … me encargo de esto. No hay problema.
Y genial. Ahora no se compilará. ¿¿Por qué?? Oh, ya veo … Este objeto contiene este otro objeto. Así que también voy a necesitar eso. No hay problema.
Espera … no solo necesito ese objeto. Necesito el padre del objeto y el padre de su padre y así sucesivamente con cada objeto contenido y TODOS los padres de lo que contienen junto con sus padres, padres, padres …
Ugh
Hay una gran cita de Joe Armstrong , el creador de Erlang:
El problema con los lenguajes orientados a objetos es que tienen todo este entorno implícito que llevan consigo. Querías un plátano, pero lo que obtuviste fue un gorila sosteniendo el plátano y toda la jungla.
Banana Monkey Jungle Solution
Puedo domar este problema al no crear jerarquías que sean demasiado profundas. Pero si la herencia es la clave para la reutilización, entonces cualquier límite que coloque en ese mecanismo seguramente limitará los beneficios de la reutilización. ¿Correcto?
Correcto.
Entonces, ¿qué debe hacer un pobre Programador Orientado a Objetos, que ha tenido una buena ayuda de Kool-aid?
Contener y delegar. Más sobre esto más tarde.
El problema del diamante
Tarde o temprano, el siguiente problema hará que su cabeza se vuelva fea y, según el idioma, irresoluble.
La mayoría de los lenguajes OO no son compatibles con esto, a pesar de que esto parece tener sentido lógico. ¿Qué es tan difícil de soportar esto en los idiomas OO?
Bueno, imagina el siguiente pseudocódigo:
Class PoweredDevice {
}Class Scanner inherits from PoweredDevice {
function start() {
}
}Class Printer inherits from PoweredDevice {
function start() {
}
}Class Copier inherits from Scanner, Printer {
}
Observe que tanto la clase Scanner como la clase Printer implementan una función llamada start .
Entonces, ¿qué función de inicio hereda la clase Copier ? ¿El escáner ? ¿La impresora ? No pueden ser los dos.
La solución de diamante
La solución es simple. No hagas eso.
Si eso es correcto. La mayoría de los idiomas OO no te permiten hacer esto.
Pero, pero … ¿y si tengo que modelar esto? ¡Quiero mi reutilización!
Entonces debe contener y delegar .
lass PoweredDevice {
}Class Scanner inherits from PoweredDevice {
function start() {
}
}Class Printer inherits from PoweredDevice {
function start() {
}
}Class Copier {
Scanner scanner
Printer printer
function start() {
printer.start()
}
}
Observe aquí que la clase Copiadora ahora contiene una instancia de una Impresora y un Escáner . Delega la función de inicio a la implementación de la clase Impresora . Podría delegarse fácilmente en el escáner .
Este problema es otra grieta en el pilar de herencia.
El problema de la clase base frágil
Así que estoy haciendo mis jerarquías superficiales y evitando que sean cíclicas. No hay diamantes para mí.
Y todo estaba bien con el mundo. Eso es hasta …
Un día, mi código funciona y al día siguiente deja de funcionar. Aquí está el pateador. No cambié mi código .
Bueno, tal vez es un error … Pero espera … Algo cambió …
Pero no estaba en mi código . Resulta que el cambio estaba en la clase de la que heredé.
¿Cómo podría un cambio en la clase Base romper mi código?
Así es como…
Imagine la siguiente clase Base (está escrita en Java, pero debería ser fácil de entender si no conoce Java):
import java.util.ArrayList;
public class Array
{
private ArrayList<Object> a = new ArrayList<Object>();
public void add(Object element)
{
a.add(element);
}
public void addAll(Object elements[])
{
for (int i = 0; i < elements.length; ++i)
a.add(elements[i]); // this line is going to be changed
}
}
public class Array
{
private ArrayList<Object> a = new ArrayList<Object>();
public void add(Object element)
{
a.add(element);
}
public void addAll(Object elements[])
{
for (int i = 0; i < elements.length; ++i)
a.add(elements[i]); // this line is going to be changed
}
}
IMPORTANTE : Observe la línea de código comentada. Esta línea se cambiará más tarde, lo que romperá las cosas.
Esta clase tiene 2 funciones en su interfaz, add () y addAll () . La función add () agregará un solo elemento y addAll () agregará varios elementos llamando a la función add .
Y aquí está la clase Derivada:
public class ArrayCount extends Array
{
private int count = 0;
@Override
public void add(Object element)
{
super.add(element);
++count;
}
@Override
public void addAll(Object elements[])
{
super.addAll(elements);
count += elements.length;
}
}
La clase ArrayCount es una especialización de la clase Array general . La única diferencia de comportamiento es que ArrayCount mantiene un recuento del número de elementos.
Veamos ambas clases en detalle.
La matriz add () añade un elemento a un local de ArrayList .
El Array addAll () llama al complemento ArrayList local para cada elemento.
El Array addAll () llama al complemento ArrayList local para cada elemento.
El ArrayCount add () llama de su matriz add () y después se incrementa el recuento . El ArrayCount addAll () llama de su matriz addAll () y después se incrementa el recuento por el número de elementos.
Y todo funciona bien.
Ahora para el cambio de última hora . La línea de código comentada en la clase Base se cambia a la siguiente:
public void addAll (Elementos de objeto [])
{
for (int i = 0; i <elements.length; ++ i)
add (elements [i]); // esta línea fue cambiada
}
En lo que respecta al propietario de la clase Base, todavía funciona como se anuncia. Y todas las pruebas automatizadas aún pasan .
Pero el propietario es ajeno a la clase Derivada. Y el propietario de la clase Derivado se encontrará con un rudo despertar.
Ahora ArrayCount addAll () llama de su matriz addAll () , que internamente llama a la suma () , que ha sido anulado por el Derivado clase.
Esto hace que el recuento se incremente cada vez que se llama a la clase Derivada add () y luego se incrementa OTRA VEZ por el número de elementos que se agregaron en la clase Derivada addAll () .
SE CUENTA DOS VECES.
Si esto puede suceder, y lo hace, el autor de la clase Derivada debe SABER cómo se ha implementado la clase Base. Y deben estar informados sobre cada cambio en la clase Base, ya que podría romper su clase Derivada de maneras impredecibles.
Ugh! Esta enorme grieta amenaza para siempre la estabilidad del precioso pilar de herencia.
La solución de clase base frágil
Una vez más Contener y Delegar al rescate.
Al usar Contain and Delegate, pasamos de la programación de White Box a la programación de Black Box. Con la programación de White Box, tenemos que mirar la implementación de la clase base.
Con la programación de Black Box, podemos ignorar por completo la implementación, ya que no podemos inyectar código en la clase Base al anular una de sus funciones. Solo tenemos que preocuparnos por la interfaz.
Esta tendencia es inquietante …
Se suponía que la herencia sería una gran victoria para Reuse.
Los lenguajes orientados a objetos no hacen que Contain and Delegate sea fácil de hacer. Fueron diseñados para facilitar la herencia.
Si eres como yo, estás empezando a preguntarte sobre este asunto de la herencia. Pero lo más importante, esto debería sacudir su confianza en el poder de la Clasificación a través de Jerarquías.
El problema de la jerarquía
Cada vez que comienzo en una nueva empresa, lucho con el problema cuando estoy creando un lugar para colocar los Documentos de mi Empresa, por ejemplo, el Manual del Empleado.
¿Creo una carpeta llamada Documentos y luego creo una carpeta llamada Compañía?
¿O creo una carpeta llamada Compañía y luego creo una carpeta llamada Documentos?
Ambos trabajan. Pero cual es la correcta? ¿Cuál es el mejor?
La idea de las jerarquías categóricas era que había clases base (padres) que eran más generales y que las clases derivadas (niños) eran versiones más especializadas de esas clases. Y aún más especializado a medida que avanzamos por la cadena de herencia. (Ver la jerarquía de formas arriba)
Pero si un padre y su hijo pueden cambiar de lugar arbitrariamente, entonces claramente algo está mal con este modelo.
La solución de la jerarquía
Lo que está mal es …
Las jerarquías categóricas no funcionan .
Entonces, ¿para qué sirven las jerarquías?
Muy bien por ti, haz echo un análisis muy bueno criticando la POO, lo que no encontré fue tu aporte. Me quedé esperando cual es tu propuesta? . Si me traes el problema al menos proponme una solución..