Home » Java » Java y la programación orientada a objetos (POO)

Java y la programación orientada a objetos (POO)

En este artículo veremos los conceptos básicos de la programación orientada a objetos:
 
Java es un lenguaje que se adhiere plenamente a los preceptos de la programación orientada a objetos. Por esta razón es imprescindible que el aprendizaje del lenguaje Java sea precedido con una revisión de los conceptos relacionados a la POO y a su terminología. Ahora veremos que:
  • La POO no es tan compleja ni confusa y que antes que nada es lógica.
  • La POO se basa en una corta lista de conceptos fundamentales: abstracción, estado, comportamiento, encapsulación y herencia.
  • Los objetos se comunican entre sí por medio de mensajes de acuerdo a sus interfaces.
  • La POO tiene en la herencia un mecanismo sobresaliente para la reutilización del código.
  

Java es un lenguaje que permite la programación orientada a objetos y es necesario, desde el mismo inicio del aprendizaje del lenguaje, que se conozcan sus fundamentos, empezando por la terminología.  

 

Terminología de la programación orientada a objetos

Los programadores que utilizan la metodología de la orientación a objetos observan todo en términos de objetos:
  • Analizan el aspecto del objeto
  • Cómo actúan
  • Cómo se relacionan con otros objetos  

Pero… ¿qué es un objeto? 

La respuesta es tan simple que nos deja perplejos: estamos rodeados de objetos y todo lo que tocamos y lo que no tocamos puede ser visto o considerado un objeto. Todo lo que necesitemos para resolver nuestro problema lógico se puede definir como objeto.

Si estamos definiendo un programa de nómina tendremos un objeto Empleado, un objeto Cuenta, un objeto Empresa, un objeto Liquidación, etc. Los objetos que implementaremos no son fijos, nuestra solución de la aplicación Nómina quizá utilice otros objetos diferentes y seguirá siendo una aplicación igualmente válida y funcional.

En la definición de nuestros programas representaremos los objetos en forma de código y para ello crearemos lo que se denominan entidades.
Cada objeto tiene características propias que lo distingue del resto. Veamos un objeto Empleado:
 
  • Tiene un nombre y apellido
  • Tiene un estado civil
  • Tiene un domicilio
  • Tiene un documento de identidad
  • Tiene un color de ojos determinado
  • Tiene un peso determinado
  • Tiene un sueldo determinado
  • Pertenece a un departamento de la empresa
  • Sabe hacer determinadas cosas (programar, conducir, enseñar, etc.)
Todas estas características, y muchas otras que no se mencionan, definen a un objeto Empleado.
 
En la programación orientada a objetos estas características se denominan: estado y comportamiento.
Un concepto fundamental de la programación orientada a objetos es su capacidad para representar los datos y las funciones que operan sobre esos datos en un elemento de software que se denomina objeto. Justamente al unir en un único envoltorio los datos y las funciones (o dicho en otros términos, las propiedades y los métodos, o lo que es lo mismo, el estado y el comportamiento) estamos creando un objeto único. 
Tal como puede verse estamos utilizando la palabra dato de un objeto como sinónimo de propiedad y de estado: son tres maneras de referirnos a lo mismo. Del mismo modo, al utilizar la palabra función la empleamos como sinónimo de método y comportamiento
En la bibliografía de la programación orientada a objetos nos encontraremos estas variantes de terminología (método, función, comportamiento y para dato, propiedad, estado) pero no nos confundamos: son distintas maneras de decir lo mismo.
 
No sólo un objeto Empleado tiene estado y comportamiento, si lo analizamos detenidamente todos los objetos tienen alguna clase de estado y de comportamiento. Obviamente, no todos los objetos comparten las mismas propiedades y métodos. Un coche se mueve pero no vuela (salvo desgraciadas excepciones) pero un avión hace ambas cosas y una pirámide ninguna de esas dos. Estos tres objetos tan diferentes entre sí pueden tener más de una propiedad en común (peso y color, por ejemplo) y más de un método en común (abrir y cerrar).
 
En términos generales, estado o propiedad o dato del objeto, es lo que define lo que es o posee un objeto mientras que comportamiento, función o método del objeto  es lo que define lo que puede hacer el objeto. 
 
El éxito de un buen diseño de los objetos depende fundamentalmente  de la capacidad de abstracción que logremos para reconocer qué objetos definiremos para resolver nuestro problema.

Propiedades y métodos

En Java, tal como se puede prever por lo visto en la sección anterior, el estado de un objeto queda definido por el valor de unas variables especiales denominadas propiedades. Por su parte, el comportamiento se implementa al desarrollar los métodos de un objeto. En realidad se podría pensar que esto de la programación orientada a objetos no ha inventado nada, ya que variables y funciones existen desde que se creó el primer programa informático. No es así, el aporte de la POO es que combina estos dos conceptos en un único objeto. 
 
Los programas tradicionales, no POO, también utilizan funciones para realizar acciones que generalmente se definen al principio o al final del programa y se las llama cada vez que sean necesarias mientras que en la POO las funciones (métodos) están incrustadas dentro de los propios objetos.
Supongamos que tenemos que codificar un programa POO que modela o simula el comportamiento de un objeto Televisor. Por supuesto al recibir este requerimiento comenzamos a pensar sobre qué es un televisor, qué podemos ordenarle que haga y qué logramos con esas órdenes. 
 
Al diseñar objetos de software debemos hacer un profundo ejercicio de abstracción, ir a lo fundamental del objeto y no perdernos en los detalles de la implementación. Aunque es un ejercicio que practicamos a cada momento sin darnos cuenta a veces es difícil abstraer lo importante de un objeto y descartar lo trivial, pero si no lo hacemos adecuadamente nuestros objetos quedarán diseñados con gran cantidad de datos irrelevantes y de métodos prescindibles o que no corresponden a comportamientos naturales del objeto. Por ejemplo, podríamos llegar al absurdo de implementar el método LevarAnclas para un objeto de clase Avión. Primero debemos identificar las funciones esenciales del objeto y después elegir sólo los datos que son importantes y necesarios para realizar esas funciones.
 
Siguiendo con nuestro ejemplo práctico y esquematizando el funcionamiento de un televisor podríamos decir que:
 
  • Está encendido/apagado
  • Tiene un programa
  • Tiene un volumen de sonido
  • Tiene un contraste
  • Tiene un brillo
 
Estas características definen el estado de un televisor. Pero para cambiar estos estados necesitamos poder realizar ciertas acciones:
 
  • Encender/Apagar
  • Cambiar de programa
  • Modificar el volumen
  • Modificar el contraste
  • Modificar el brillo
 
Podríamos decir entonces que cada estado del televisor queda bajo la protección o control de una acción. En este ejemplo queda claro que lo descriptivo es el estado y lo operativo es la acción. 
 
Si tuviésemos que desarrollar este programa simulador sin seguir las técnicas POO ya estaríamos codificando funciones para sacarnos de encima el problema.Este sería un grave error en POO: en POO tenemos que ser mucho más reflexivos y analizar el objeto a fondo antes de crear la clase que lo definirá.

librophp006

Este dibujo esquematiza el ideal en la definición de objetos (no siempre se cumple): a los datos no se accede directamente desde fuera del objeto, sino que se debe pasar por los métodos "protectores" que saben cómo tratar sus propiedades y cómo validar su contenido.
Volvamos al ejemplo del televisor. Supongamos que queremos cambiar de programa, para ello tenemos un método denominado "Cambiar el programa" que sabe perfectamente qué programas tiene disponible el objeto Televisor al que pertenece. Este pseudo-código puede hacer esa acción:
 
TV.CambiarPrograma(6);

 
Y el método CambiarPrograma(6) verificará el valor de la propiedad Programa y la asignará al valor disponible siguiente, pasando por ejemplo del canal 5 al 6. El método CambiarPrograma() jamás asignará un valor incorrecto a la propiedad Programa porque sabe perfectamente el rango de valores válidos de los programas de televisión (por ejemplo, las cadenas válidas sólo son 1,2,3,5,6,7). Si el parámetro pasado hubiese estado fuera de los valores correctos la acción se habría rechazado y la propiedad Programa habría quedado indemne manteniendo su valor anterior.
Ahora veamos un caso en el que el programador quiere acceder directamente al dato del objeto sin pasar por los métodos "protectores". Supongamos que en este pseudo-código se intenta cambiar la propiedad Programa:
 
TV.Programa = 94;

 
El programador desconoce si el programa 94 existe o no en el televisor y probablemente tendremos un error de ejecución.
La notación utilizada en este pseudo-código es similar a la empleada por el lenguaje C para acceder a un elemento de una estructura. El punto se utiliza para separar al objeto del método y los parámetros, cuando los hay, se pasan entre paréntesis.
¿Puede un programador modificar un dato de un objeto evitando el uso de los métodos del propio objeto? Si el objeto estuviese bien definido los datos tendrían que ser inaccesibles y sólo podrían ser modificados mediante un método del objeto. Esta capacidad para ocultar la información de un objeto se denomina encapsulación y es uno de los pilares de la POO. 
 
Encapsulación es la capacidad para ocultar los datos de un objeto para que sean inaccesibles a todo el mundo salvo a los métodos del propio objeto.
La encapsulación no sólo afecta a los datos, también se aplica a los detalles de la implementación de los métodos. Un objeto se comunica con el resto del mundo mediante su interfaz, que no es otra cosa que la lista de métodos y propiedades que hace públicos. A nadie le interesa cómo hace una determinada acción sino simplemente que la haga. Así como al usuario típico del televisor no le importa en absoluto todo lo que ocurre en el televisor cuando cambia de cadena con el control remoto, simplemente quiere que se visualice lo más pronto posible la cadena buscada, a los usuarios de los objetos no les preocupa la implementación interna de un método, sólo que exista y que en el futuro no cambie. 
 
¿Que se quiere decir con que en el futuro no cambie? ¿Acaso los programas son inmodificables?
 
Todos sabemos que no. Pero cuando se trabaja en un entorno distribuido y dinámico modificar un objeto en producción puede ser un problema. Una vez que hacemos público un objeto con una determinada interfaz, por ejemplo, el objeto Televisor con sus métodos Apagar, Encender, CambiarPrograma, etc., y después que ese objeto se utiliza en infinitos programas ya no podemos eliminar alegremente un método del objeto. La interfaz del objeto debe mantenerse (puede tener más métodos pero no eliminar los existentes) para que siga existiendo la compatibilidad con todos los que utilicen el objeto. 
Está claro que una vez que publiquemos el objeto Televisor  no podremos eliminar, por ejemplo, el método CambiarPrograma() si no queremos tener la captura recomendada de la comunidad de desarrolladores. Pero, ¿a quién le interesa o le preocupa la implementación interna del método CambiarPrograma()? A nadie, siempre que  el método haga lo que se espera. Esto también es encapsulación, es lo que permite que los objetos puedan ser vistos como "cajas negras".

Comunicación entre objetos

En un programa en donde todo son objetos ¿cómo se realizan las tareas? Hemos visto que los objetos reaccionan a las órdenes que  saben procesar. De nada sirve que ejecutemos el método Lavar() en el objeto Televisor ya que no pasaremos de la etapa de compilación. Sólo podemos utilizar lo que la interfaz del objeto nos permita. Pero los objetos para realizar sus funciones también necesitan de la colaboración de otros objetos y estos por su parte de otros objetos: este encadenamiento de funciones se logra mediante el envío de mensajes entre los objetos que no son otra cosa que invocaciones a métodos con los parámetros que cada método necesita.
Lo más interesante de este modo de comunicación mediante mensajes es que cualquier objeto puede enviar un mensaje a cualquier otro sin importarle dónde reside el objeto, puede estar en el propio servidor o en un servidor en las antípodas, el formato del mensaje será el mismo y la complejidad de la programación también. 

Clases 

La clasificación es la tarea de agrupar cosas según sus características y sus capacidades y es otro de los conceptos fundamentales de la POO. Somos incapaces de omitir la clasificación de las cosas, cualquier nuevo elemento se debe clasificar en algún sitio, es una actividad inherente al ser humano. Si se descubre un nuevo elemento químico inmediatamente se lo colocará en la tabla de elementos. Si se descubre una nueva especie animal, el biólogo de turno se encargará de clasificarlo correspondientemente. 
En la POO ocurre lo mismo, todo debe estar clasificado, es decir, cada objeto debe pertenecer a una clase específica de acuerdo a sus características y si esa clase no existe, se crea. 
Muchas veces se verá que se utiliza la palabra Objeto y Clase de modo indistinto e intercambiable. No es incorrecto pero en realidad hay un matiz que diferencia a estos dos conceptos. Una clase es algo así como una plantilla o molde  a partir de la que se crean los objetos. Esta clase posee todas las variables y funciones que debe tener un objeto de su clase, luego el objeto tendrá valores propios en esas variables cuando el objeto se haya creado.
Por lo tanto, un objeto no es una clase, sino que es el resultado de crear un ejemplar de esa clase. 
La estructuras del lenguaje C se puede comparar a un objeto, ya que después de su definición podemos crear todas las variables del tipo de la estructura definida, pero para ser realmente una clase le falta la capacidad para tener funciones incrustadas y una interfaz para que los otros objetos sepan comunicarse con ella.
En Java una clase toma este formato:
 
class Nombre{
... variables
... métodos
}

La palabra clave class siempre encabeza la definición de una clase y la definición de variables y métodos queda encerrada entre llaves.

Accesibilidad 

Ya sabemos que la encapsulación es la capacidad de ocultar los miembros de una clase, y al usar la palabra miembros nos referimos tanto a las propiedades como a los métodos de una clase. Para que los métodos actúen como "protectores" del contenido de las propiedades es preciso impedir que un programador utilice una instrucción como esta:
 
TV.Programa(94);

Y para evitarlo se restringe la accesibilidad a los datos (también puede hacerse a los métodos) definiéndolos como private (lo contrario sería public). Cuando se crea un objeto de una clase, desde afuera del objeto sólo podemos acceder o usar sus miembros públicos, por lo tanto, como regla general se definen las variables como private y los métodos como public (salvo que también nos interese impedir el uso de un método, en tal caso también lo tendríamos que definir como private). Pero esto sólo es una regla general para lograr la encapsulación sin esfuerzo.
En realidad, Java afina mucho más la puntería sobre este tema y tiene cuatro niveles de acceso: public, private, protected y friendly.
Por el momento es suficiente con percibir la diferencia entre public y private.   

Creación de ejemplares de una clase

La definición de una clase es una acción declarativa, es decir, en el programa no sucede nada hasta que no creamos un objeto tomando como plantilla una clase determinada (definida por Java, por terceros o por nosotros). La creación de un ejemplar de una clase se suele denominar instanciación, palabra que proviene del inglés instantiate y que da lugar al uso de la palabra instancia para denominar un ejemplar de una clase. La comodidad de los traductores o la facilidad que tienen los informáticos de habla hispana a crear neologismos dio lugar a la aparición de la palabra instancia que puede confundir a los menos expertos.  
Para crear un ejemplar de la clase TV se utiliza esta sintaxis:
 
TV miTV = new TV();

El operador new seguido con el nombre de una clase válida crea un objeto de dicha clase en una posición de memoria a la que nos referiremos con el nombre miTV
Cuando se ejecuta esta instrucción se produce una invocación al constructor de la clase (TV()), que no es otra cosa que un método de la clase que se encarga de las tareas de inicialización del ejemplar. 
Después de la creación del ejemplar de la clase TV podemos usarlo libremente siempre dentro de lo que nos permita su interfaz, por ejemplo:
 
miTV.Encender(); // uso el método para encender el televisor
miTV.CambiarPrograma(2); // sintoniza la cadena 2
miTV.Apagar(); // como de costumbre, no hay nada para ver

Es un ejemplo simple pero da una idea básica sobre cómo  se crea y utiliza un objeto.

Herencia

Dejamos para el final el concepto más interesante de la POO: la herencia. Ya sabemos que en la POO podemos crear objetos a partir de la nada, ahora es el momento de saber aprovechar los objetos que se han creado de la nada para crear  objetos más complejos o especializados. Esto se logra con el mecanismo de la herencia: la mejor solución para la reutilización del código.
 
Supongamos que hemos creado una espléndida clase para definir una persona, con todos los datos que comparten todas las personas y todas los métodos necesarios para gestionarlas, y posteriormente nos toca desarrollar una aplicación de nóminas. En las nóminas hay empleados y los empleados son, antes que nada, personas.
¿Qué más fácil que definir la clase Empleado basada en la clase Persona?
La clase Empleado es una especialización de la clase Persona, es decir, tiene todos los miembros de la clase Persona más los miembros propios de la clase Empleado (por ejemplo, el sueldo). Todas las personas tienen nombre y apellido y documento pero no todas tienen sueldo: por eso es preciso ampliar la clase Persona y crear la clase Empleado, pero al hacerlo no lo hacemos desde cero, aprovechamos todo el código ya escrito y probado de la clase Persona; ¿y cómo se aprovecha este código? Muy fácil:
 
class Empleado extends Persona{
int Sueldo;
...
}

Así de simple. Y si en otra aplicación de alumnos nos encontramos con una situación parecida podemos reutilizar la clase Persona nuevamente:
 
class Alumno extends Persona{
int Nivel;
...
}

 
Por su parte, una clase hija, como Empleado o Alumno, se puede transformar en clase base de otra nueva clase:
 
class Gerente extends Empleado{
int Area;
...
}

No todos los empleados son gerentes, pero algunos tienen esa suerte.
Los gerentes también tienen sueldo, como los empleados (por supuesto, no el mismo), y también tienen nombre y apellido como todas las personas.
Por lo tanto, la clase Gerente es una clase especializada de las clases Empleado y Persona.
La gran virtud del uso de la herencia es que permite la reutilización del código de un modo simple y dinámico (las clases pueden ser modificadas pero el enlace se mantiene y mientras la interfaz sea la misma no se presentarán problemas de compatibilidad de versiones). No sólo se reutiliza el código sino que el código resultante tiene mayor fiabilidad ya que se minimiza la codificación.
Una clase no está restringida al comportamiento de su clase base (o padre), sino que además define un comportamiento propio (que no tiene su clase base) que hace que la clase sea más específica. 

Jerarquía de clases

La subclasificación es la especialización de una clase mediante la creación de una clase hija basada en una clase madre. Este proceso lleva a lo que se denomina árbol de herencia o jerarquía de clases. En el nivel superior la clase es más genérica y a medida que se realizan subclasificaciones se convierte en una entidad más especializada: una persona es más genérica que un empleado y éste es más genérico que un gerente. 
librophp007

izq sup der

Deja un comentario