8 de mayo de 2017

Herencia vs. composición (listas).

   Uno de los aspectos más importantes de la programación orientada a objetos es la conveniencia de la reutilización de código por medio de la abstracción. En este sentido, dos de los esquemas más comunes al respecto son: la herencia y la composición.

   Esta entrada muestra, por medio de un ejemplo ya conocido y presentado previamente al lector, la implementación de una pila utilizando una lista enlazada. Dicha implementación se realiza empleando los dos esquemas mencionados con anterioridad. Así mismo, se analizan las ventajas y desventajas de cada uno de ellos.

   Implementación de una pila utilizando herencia.
   La implementación de una pila por medio de una lista con un enfoque basado en la herencia es en realidad bastante simple. Para muestra, basta con ver el código del código del Ejemplo PilaH.

   Observe que la clase del Ejemplo PilaH no define atributos, y que únicamente define dos constructores y los métodos push y pop; lo cual también ha sido representado en el diagrama de clases UML de la siguiente figura.

Diagrama de clases UML para la implementación de una pila utilizando herencia y una lista enlazada.
 
    Insto nueva y amablemente al lector a que compare, contraste, y analice con detenimiento antes de continuar, a la figura anterior con el Ejemplo PilaH.

   Note que los métodos push (líneas 14-16) y pop (líneas 18-20) del Ejemplo PilaH no hacen otra cosa más que encapsular el comportamiento de los métodos insertaAlInicio y eliminaDelInicio respectivamente, los cuales son servicios o comportamiento definidos en la clase Lista del Ejemplo Lista. Note también que es posible acceder a dichos métodos debido a que la clase PilaH hereda de la clase Lista (línea 5).

   Observe que como parte del mecanismo de la herencia, tampoco es necesario definir el comportamiento de los métodos estaVacia e imprime dentro de la clase PilaH, ya que se encuentran definidos en la clase Lista.

   El primero de dichos comportamientos forma parte de la definición de las primitivas de la estructura de datos pila, mientras que el segundo es utilizado en la clase PruebaPilaH del Ejemplo PruebaPilaH (líneas 11 y 20), la cual es la clase de prueba del Ejemplo en cuestión.

La salida del Ejemplo PruebaPilaH se muestra en la siguiente figura:

Salida del Ejemplo PruebaPilaH.
 
    Consideraciones.
   Al menos en apariencia, el mecanismo de la herencia resulta sumamente conveniente con base en lo expuesto con anterioridad; sin embargo, presenta algunos inconvenientes potenciales que vale la pena considerar.

   Las instancias de la clase PilaH, al heredar las características y el comportamiento inherentes a una lista enlazada, pueden hacer uso de los métodos de inserción y de eliminación correspondientes a ésta, por lo que desde esta perspectiva, un objeto de dicha clase podría permitir inserciones y eliminaciones, no únicamente del tope de la pila (representado por inicio), sino también de la base de la pila ("representada" por fin) a la que se supone, por definición de pila, no se debe tener acceso.

   Tome en consideración que todavía se podría transgredir más la definición de una pila, ya que potencialmente es posible también insertar en, o eliminar de cualquier parte de la estructura de datos con las modificaciones correspondientes (vea el Ejercicio 2.2 de los Ejercicios selectos), lo cual queda completamente fuera tanto de la definición que se hizo de la pila, como de las operaciones primitivas que le son inherentes.

Implementación de una pila utilizando composición.
   La implementación de una pila utilizando el enfoque de composición, se basa en la idea de que una lista enlazada contiene ya definidas las operaciones que necesita una pila, pero que también, como se discutió con anterioridad, contiene otras operaciones que no deberían ser utilizadas por la misma.

   En función de lo anterior y de manera conveniente, lo que puede hacerse es encapsular las operaciones de la lista enlazada dentro de las de la pila con la finalidad de proporcionar una interfaz o un conjunto de servicios ad hoc con la definición de la estructura de datos en cuestión: la pila.

Diagrama de clases UML para la implementación de una pila utilizando composición y una lista enlazada.
 
    La figura anterior muestra el diseño en diagrama de clases UML la definición de una pila que utiliza o contiene (has-a) una lista enlazada para su implementación. Observe y compare con detenimiento dicho diagrama con el respectivo diagrama UML para la implementación con herencia, y note que el cambio principal está en la clase PilaC y en la relación entre ésta y la clase Lista.

   Ahora bien, la definición de la clase PilaC se muestra en el Ejemplo PilaC. Insto una vez más al lector a que ponga atención en dos aspectos:
  1. La relación entre el diagrama de clases de la figura anterior y el Ejemplo PilaC.
  2. Los métodos push (líneas 16-18), pop (líneas 20-22) e imprime (líneas 24-26) no hacen otra cosa más que encapsular de manera conveniente, los mensajes enviados al objeto tope (línea 6), mismos que son llevados a cabo por los métodos correspondientes definidos en la clase Lista.
   Con base en lo anterior, los objetos instanciados de la clase PilaC sólo podrán acceder a los servicios definidos explícitamente en dicha clase y a ningún otro más independientemente de los métodos que posea la clase Lista, lo cuál hace que, desde el punto de vista de la abstracción y la representación de la estructura de datos, el enfoque de la composición sea mucho más conveniente, aunque un poco más laborioso, que el de la herencia. Por otro lado, para implementar una pila utilizando una lista enlazada la cantidad nueva de código se reduce significativamente; sin embargo, si bien es cierto que uno de los enfoques del paradigma orientado a objetos se basa en la reutilización del código, es importante que el lector se haga consciente de estos dos aspectos, ya que cada uno de ellos tiene sus ventajas y desventajas, y la decisión final de implementación debería tomarse con pleno conocimiento de causa, ya que no siempre es mejor tomar el camino más fácil (herencia).

   La clase de prueba del Ejemplo PilaC se muestra en el Ejemplo PruebaPilaC y sigue el mismo mecanismo de inserción que el los ejemplos de prueba anteriores. Finalmente, compruebe que la salida del Ejemplo PruebaPilaC coincide exactamente con la salida correspondiente del Ejemplo PruebaPilaH, y que desde el punto de vista de la ejecución, no podría saberse cuál fue el enfoque de diseño elegido.