domingo, agosto 21, 2011

Programación orientada a objetos

Programación orientada a objetos



Este texto lo saque del siguiente blog:http://blog.smaldone.com.ar/2006/10/27/programacion-orientada-a-objetos/ creo que esxplica bastante bien los conceptos de clase, objeto, herencia y polimorfismo, me parecio interesante asi que se los dejo para lo que lo lean.

Cuando comencé a explorar la programación orientada a objetos (POO u OOP, en inglés), allá por el año 1994, me ocurrió lo que a muchos. Luego de casi 10 años de programar proceduralmente (en Basic, Pascal y Cobol) me encontraba con una nueva visión. Si bien creía entender los conceptos de clase, objeto, herencia y polimorfismo, no lograba ver claramente la diferencia de enfoque a la hora de diseñar un programa. No podía apreciar realmente cuáles eran las diferencias y las similitudes entre el enfoque procedural y el orientado a objetos.

A través de mi experiencia docente varias veces me vi frente a la tarea de introducir los conceptos básicos de la POO, encontrándome con la misma situación, pero desde un lugar diferente: ¿Cómo explicar el nuevo enfoque a personas acostumbradas a programar de forma procedural? ¿Cómo resaltar las similitudes y las diferencias? Fue gracias a un ejemplo del libro “C++ Annotations” que encontré una forma simple de presentar la idea básica detrás de la programación orientada a objetos.


Una consideración más que necesaria: este artículo no se trata de un análisis profundo de la programación orientada a objetos, sino simplemente de una simple y breve introducción, intentando dar un panorama inicial a aquellos que no han tenido contacto con (o nunca han entendido) la POO.

Aclaraciones previas
Antes de desarrollar el ejemplo, aclararemos algunos conceptos que muchas veces se prestan a confusión:

Lenguajes imperativos: Son aquellos basados en sentencias, ya sean procedurales, orientados a objetos puros o mixtos. Entre ellos se cuentan Pascal, C, C++, Java, Fortran, Perl y Python.

Lenguajes procedurales: Son lenguajes imperativos basados en procedimientos (o rutinas) y funciones. Entre ellos podemos nombrar a C, Fortran, Pascal (estándar) y Basic.

Lenguajes orientados a objetos: Son lenguajes imperativos basados en clases (algunos, llamados mixtos soportan también el modelo procedural). Entre los lenguajes orientados a objetos puros podemos nombrar a Smalltalk, Eiffel y Java. Entre los mixtos se encuentran C++ y Python.

Lenguajes funcionales: Son aquellos basados en funciones matemáticas (y no en comandos o sentencias). Podemos nombrar aquí a ML, Haskell y Lisp.

Un programa procedural simple
Vamos a plantear un ejemplo sencillo. Para ello supondremos un lenguaje imperativo con una sintaxis similar a la de Pascal (usaremos este lenguaje ficticio para simplificar la explicación, en los apéndices al final del artículo podrá encontrar ejemplos equivalentes en PHP, Python y Eiffel).

Supongamos que tenemos el siguiente programa:


program personas;
type persona = record
nombre: string;
apellido: string;
edad: integer
end;
procedure inicializar(n, a: string; e: integer; var p: persona);
begin
p.nombre := n;
p.apellido := a;
p.edad := e
end;
function es_mayor(p: persona): boolean;
begin
return p.edad >= 18
end;
function nombre_completo(p: persona): string;
begin
return p.nombre + " " + p.apellido;
end;
var
p: persona;
begin
inicializar("Juan", "Perez", 25, p);
write(nombre_completo(p));
if (es_mayor(p)) then
writeln (" es mayor de edad."
else
writeln (" es menor de edad."
end.

El programa es bastante sencillo. Primero declaramos un tipo persona que es un registro que contiene los campos nombre, apellido y edad. Luego definimos el procedimiento inicializar que toma el nombre, el apellido, la edad y la persona y asigna los primeros a los campos correspondientes de la última. Luego, un par de funciones (es_mayor y nombre_completo) toman una persona y realizan cálculos sobre los valores de sus campos.

En los programas procedurales hacemos esto todo el tiempo: definimos estructuras y tipos de datos y luego creamos procedimientos y funciones que toman como parámetros variables de estos tipos y realizan distintas operaciones sobre ellos. Dicho de otra manera: podemos ver a los programas procedurales como un conjunto de procedimientos y funciones que manipulan estructuras de datos pasadas como parámetros.

Una visión diferente
Podríamos intentar una visión distinta de la situación. Supongamos que nuestro lenguaje nos permite definir acciones y funciones dentro de un registro. Supongamos además que dichas funciones pueden acceder a todos los campos del registro. De esta forma, por ejemplo, el procedimiento inicializar no necesitaría del parámetro p (la persona a inicializar), sino que actuaría sobre la persona a la que pertenece (ya que se encuentra definido dentro del registro.

De esta forma, podríamos reescribir el programa como sigue:


program personas;
type persona = record
nombre: string;
apellido: string;
edad: integer;
procedure inicializar(n, a: string; e: integer);
begin
nombre := n;
apellido := a;
edad := e
end;
function es_mayor: boolean;
begin
return edad >= 18
end;
function nombre_completo: string;
begin
return nombre + " " + apellido;
end
end;
var
p: persona;
begin
p.inicializar("Juan", "Perez", 25);
write(p.nombre_completo);
if (p.es_mayor) then
writeln (" es mayor de edad."
else
writeln (" es menor de edad."
end.

Esta visión alternativa tiene varias ventajas. Inmediatamente notamos que los perfiles de los procedimientos y funciones se simplifican, debido a la eliminación del parámetro que representa a la persona sobre/con la cual se realizarán las operaciones.

Una diferencia importante de notación a la hora de usar la estructura de datos definida, es que ya no utilizamos expresiones de la forma funcion(variable), sino que ahora escribimos variable.funcion.

Una ventaja adicional es que la notación es más consistente. Al referirnos a p.nombre_completo no podemos saber (porque en realidad no interesa) si nombre_completo es un campo del registro o una función. Esto es realmente importante: en el caso del campo edad este podría ser reemplazado por una función que calcule la edad de la persona, añadiendo un campo que represente la fecha de nacimiento. De ocurrir esto, los programas que usen el tipo persona no requerirían mayores modificaciones, ya que podrían seguir haciendo referencia a p.edad. Esto facilita la independencia de la implementación y el encapsulamiento, uno de los conceptos claves de la POO.

Ajustando nuestro vocabulario
A lo que antes llamábamos tipo, refiriéndonos a estructuras de datos, ahora lo llamamos clase, entendiendo como tal no sólo las estructuras, sino también el comportamiento asociado (las acciones y funciones asociadas directamente con la estructura de datos).

A lo que antes llamábamos variable ahora lo llamamos objeto. Así como las variables son de determinado tipo, los objetos son de determinada clase.

Los procedimientos y funciones definidos dentro de una clase se llaman métodos, en tanto que los campos se denominan atributos. En nuestro ejemplo, podríamos decir que la clase persona tiene los atributos nombre, apellido y edad, y los métodos inicializar, nombre_completo y es_mayor.

Un concepto importante: la herencia
En la POO existe la posibilidad de extender el comportamiento de una clase, añadiendo atributos y métodos. El mecanismo utilizado para tal fin se denomina herencia.

Siguiendo con nuestro ejemplo, podríamos querer definir una clase empleado. Básicamente un empleado posee todas las y características de una persona (tiene nombre y apellido, tiene sentido preguntarse si es mayor de edad, etc.). Sin embargo, un empleado tendrá otros atributos (por ejemplo, un salario, un cargo) y también otros métodos (liquidar_salario, etc.). Es por esto que bastará con definir a la clase empleado heredando de la clase persona (en POO se usa la expresión “un empleado es una persona“), y añadiendo el nuevo comportamiento (métodos y atributos).

El secreto: un cambio de visión
La POO no es, como muchos afirman, un paradigma distinto del de la programación procedural. Ambas son dos técnicas distintas para abordar la programación imperativa.

El secreto fundamental consiste en dejar de ver a un programa como un conjunto de acciones y funciones que modifican parámetros, para verlo como un conjunto de objetos con comportamiento, y a estos como estructuras que contienen acciones y funciones.

Si bien la POO involucra una serie de conceptos que escapan al alcance de este texto introductorio, el cambio de visión no es mucho más profundo que el pequeño truco que hemos realizado al incorporar a las funciones dentro de una estructura.

Recomendaciones finales
La POO representa un gran avance en la programación, tal como lo fue la programación estructurada en la década de los ’70. Entre sus ventajas más importante se encuentran un notable aumento de la productividad del programador y de la robustez de los programas.

Personalmente, el lenguaje orientado a objetos que más me agrada es Eiffel (además de sus características de orientación a objetos, soporta la metodología de diseño por contratos). Otro lenguaje muy bien diseñado (y de uso creciente) es Ruby. También es recomendable, y muy simple para comenzar, el lenguaje Python.

Apéndice: Un ejemplo en PHP
He agregado este apéndice siguiendo la sugerencia de un amigo (gracias, Santi) sobre la conveniencia de contar con un ejemplo concreto utilizando el lenguaje PHP, debido a que el mismo es usado por un gran número de personas sin una formación formal en programación.

El ejemplo inicial, usando programación procedural, sería como sigue:


function inicializar($nombre, $apellido, $edad) {
$persona['nombre'] = $nombre;
$persona['apellido'] = $apellido;
$persona['edad'] = $edad;
return $persona;
}
function nombre_completo($persona) {
return $persona['nombre'] . ' ' .$persona['apellido'];
}
function es_mayor($persona) {
return $persona['edad'] >= 18;
}
$p = inicializar('Juan', 'Perez', 25);
echo nombre_completo($p);
if (es_mayor($p)) {
echo " es mayor de edad.n";
} else {
echo " es menor de edad.n";
}
?>

Inmediatamente notamos una gran diferencia respecto del ejemplo en Pascal: en PHP nunca definimos un tipo persona ni su estructura. Esto dificulta la comprensión del programa, ya que solamente inspeccionando la función inicializar podemos ver qué campos contiene el arreglo asociativo persona. Todo esto se debe a que PHP es un lenguaje con un sistema de tipos dinámico (una característica que lo hace muy simple y rápido para pequeños desarrollos, pero que se vuelve un arma de doble filo en programas complejos).

La versión orientada a objetos de este mismo programa sería la siguiente:


class Persona {
var $nombre;
var $apellido;
var $edad;
function Persona($nombre, $apellido, $edad) {
$this->nombre = $nombre;
$this->apellido = $apellido;
$this->edad = $edad;
}
function nombre_completo() {
return $this->nombre . ' ' . $this->apellido;
}
function es_mayor($persona) {
return $this->edad >= 18;
}
}
$p = new Persona('Juan', 'Perez', 25);
echo $p->nombre_completo();
if ($p->es_mayor) {
echo " es mayor de edad.n";
} else {
echo " es menor de edad.n";
}
?>

Este ejemplo nos permite introducir otro concepto de la POO: el uso de constructores. Un constructor es una función de una clase que se ejecuta automáticamente al crear un objeto (instancia). En el caso de PHP (y de varios otros lenguajes) el constructor debe tener el mismo nombre que la clase (en nuestro ejemplo, Persona).

La sentencia $p = new Persona(“Juan”, “Perez”, 25); crea un nuevo objeto ($p), de la clase Persona, ejecutando la función Persona (que reemplaza a la antigua función inicializar) con los parámetros “Juan”, “Perez” y 25.

Otra diferencia respecto del ejemplo anterior, es la utilización del operador -> como selector de métodos y atributos de un objeto. En la definición de la clase Persona, $this es una referencia al objeto actual, necesaria para determinar el alcance de las variables utilizadas (para acceder al atributo nombre de la clase, debemos escribir $this->nombre, ya que $nombre sería interpretado como una nueva variable).

Si usted programa en PHP le recomiendo la utilización de POO (en particular, la versión 5 mejora mucho las características de orientación a objetos del lenguaje).

Apéndice: un ejemplo en Python
A continuación, un ejemplo de la versión orientada a objetos utilizando el lenguaje Python:


class Persona:
def __init__(self, nombre, apellido, edad):
self.nombre = nombre
self.apellido = apellido
self.edad = edad
def nombre_completo(self):
return self.nombre + ' ' + self.apellido
def es_mayor(self):
return self.edad >= 18
p = Persona('Juan', 'Perez', 25)
print p.nombre_completo(),
if p.es_mayor:
print "es mayor de edad."
else:
print "es menor de edad."

Como podemos apreciar, el constructor de la clase se declara como __init__, en tanto que la referencia al objeto actual se llama self (y debe aparecer como primer parámetro de todos los métodos de la clase, aunque no se utiliza en la invocación de los mismos).

Python es un lenguaje muy simple, pero caben aquí las mismas consideraciones hechas para PHP, respecto de los lenguajes con sistemas de tipos dinámicos.

Apéndice: un ejemplo en Eiffel
Finalmente, un ejemplo en mi lenguaje de programación orientado a objetos favorito: Eiffel. A continuación la definición de la clase persona:


class PERSONA
creation make
feature
nombre: STRING;
apellido: STRING;
edad: INTEGER;
make(n, a: STRING; e: INTEGER) is
do
nombre := n
apellido := a
edad := e
end
nombre_completo: STRING is
do
Result := nombre + " " + apellido
end
es_mayor: BOOLEAN is
do
Result := edad >= 18
end
end

Debemos recordar que Eiffel (a diferencia de los demás lenguajes utilizados como ejemplo) es totalmente orientado a objetos (o puro, como suele decirse). Es por esto que no hay diferencia entre las clases y el “programa principal“, siendo este también una clase (llamada raíz o ROOT).

A continuación, el código de la clase ROOT:


class ROOT
creation make
feature
make is
local
p: PERSONA
do
create p.make("Juan", "Perez", 25)
print(p.nombre_completo)
if p.es_mayor then
print(" es mayor de edad.%N"
else
print(" es menor de edad.%N"
end
end
end

Podemos notar que los nombres de clases se escriben con mayúsculas, en tanto que el lenguaje permite especificar cuál es el constructor de cada clase (en este caso, el método make).

Vemos también que sin ver el código de la clase PERSONA es imposible saber si nombre_completo es un atributo (variable) o un método (función). Esto, como habíamos señalado al comienzo del artículo, es una ventaja (notese que en PHP y en Python no ocurre lo mismo), ya que podemos cambiar la implementación de la clase, sin alterar el resto del programa.

Al tener un sistema de tipos fuerte y estático, Eiffel nos obliga a declarar el tipo de cada variable o parámetro utilizado, permitiendo así que el compilador detecte posibles errores antes de la ejecución del programa.

No hay comentarios.: