PROGRAMACION ORIENTADA A OBJETOS

Page 1

PROGRAMACIÓN ORIENTADA A OBJETOS La programación orientada a objetos o POO (OOP según sus siglas en inglés) es un paradigma de programación que usa objetos y sus interacciones, para diseñar aplicaciones y programas de ordenador. Está basado en varias técnicas, incluyendo herencia, abstracción, polimorfismo y encapsulamiento. Su uso se popularizó a principios de la década de 1990. En la actualidad, existe variedad de lenguajes de programación que soportan la orientación a objetos. ¿QUÉ ES ESO DE LOS "OBJETOS"?

En la programación tradicional existía una clara diferenciación entre los datos y su manipulación, es decir, el conjunto de algoritmos para manejarlos. Los tipos de datos eran muy simples; todo lo más números de diverso tipo y caracteres (aislados o agrupados en cadenas o matrices), pero nunca elementos heterogéneos. La situación para el programador era que el lenguaje parecía decirle: estos son los posibles contenedores para la información que debes manejar (los tipos de datos soportados por el lenguaje) y aquí aparte, las operaciones que puedes realizar con ellos (las operaciones definidas para dichos datos). Por ejemplo, si son datos numéricos las operaciones aritméticas, las comparaciones, las asignaciones, etc. Con estas herramientas el programador construía su código, y para poner un poco de orden en la maraña de algoritmos solo tenía el recurso de agruparlo en funciones. Si se tenía que hacer un programa de gestión comercial, la información correspondiente a un concepto más o menos real, por ejemplo un "cliente", tenía que ser desmenuzada en una serie de datos aislados: nombre, domicilio, población, provincia, teléfono, condiciones de compra, saldo, tipo de descuento, número de vendedor asignado, fecha de última compra, etc. etc. Luego, para hacer una factura, todos estos elementos eran tomados y procesados individualmente. Un primer intento de construir tipos de datos más complejos, que se parecieran a las situaciones del mundo real y que pudieran ser tratados (más o menos) como una unidad, lo constituyen, entre otros, las "estructuras" del C clásico ("registros" en otros lenguajes). Una estructura es una agregación de datos de diverso tipo construida a criterio del programador mediante la agrupación de elementos ya incluidos en el lenguaje (básicamente los antedichos números y caracteres). Esto supuso un paso en el nivel de abstracción de los datos, aunque las herramientas para manipularlos seguían siendo elementos totalmente diferenciados.


En esta etapa ya podía crearse una estructura "cliente" que albergara toda la información antes dispersa, pero aparte de leer y escribir la información en un solo bloque, poco más podía hacerse, los datos tenían que seguir siendo procesados individualmente con expresiones parecidas a: cliente->fecha_ult_compra = date(); tot_factura = suma * cliente->descuento cliente->saldo = cliente->saldo + tot_factura; Ante esta situación surge una nueva idea: crear nuevos tipos de datos agrupando los tipos tradicionales en entidades más complejas que reciben el nombre genérico de clases. Las clases son conjuntos de datos definidos por el programador, parecidos a las mencionadas "estructuras", pero con la importante novedad de incluir también las herramientas necesarias para su manipulación; es decir: operadores y funciones de la programación tradicional. Dicho de otro modo: los nuevos tipos de datos pueden ir acompañados de su "álgebra"; de las operaciones que se pueden realizar con ellos. La idea es construir una especie de caja negra que incluya no solo los datos, también los algoritmos necesarios para su manipulación (sus operaciones), de forma que pueda ser vista desde el exterior (el programador que las usa) como un ente al que se le envían preguntas u órdenes y responde con el resultado [2]. Este comportamiento de la clase frente al mundo exterior es lo que se llama su interfaz o protocolo. Cómo es el detalle de su interior no le interesa al usuarioprogramador en absoluto, solo le interesa conocer la forma de "comunicarse" con ella y que posibilidades le ofrece. El hecho de empaquetar juntos datos y funcionalidad, dejando visible al exterior solo lo estrictamente necesario, se denomina encapsulamiento y es otro de los distintivos de la POO. Para ilustrar la idea en un caso concreto consideremos un ejemplo clásico: Supongamos que en un programa tradicional hay que manejar números complejos. La solución es definir dos variables fraccionarias (de coma flotante) R e Y para representar respectivamente las partes reales e imaginaria de cada complejo. Luego para obtener, por ejemplo la suma (Rr, Yr), de dos complejos (R1, Y1), (R2, Y2), tenemos que codificar: Rr = R1 + R2 Yr = Y1 + Y2 para calcular el valor del módulo m tenemos que hacer: m = SQRT( (Rr * Rr) + ( Yr * Yr) ) así sucesivamente para cualquier manipulación que queramos efectuar.


Supongamos ahora que tenemos esta posibilidad de "construir" un nuevo tipo de variable, que denominaremos la clase de los complejos (la designamos por Cc), y que hemos incluido en dicha clase la información necesaria para realizar todas las operaciones permitidas en el álgebra de los números complejos. No solo la suma, también la asignación, la comparación, el cálculo del módulo, etc. Incluso los procedimientos para crear elementos nuevos (complejos) y para destruirlos (eliminarlos de la memoria). Ahora el funcionamiento sería algo parecido a: Cc x = (R1, Y1); Cc y = (R2, Y2); Estamos declarando que x e y son objetos de la nueva clase y les estamos asignando los valores correspondientes. Fíjese que estos objetos son casos concretos del nuevo tipo, la clase de los complejos (en el mismo sentido que un 5 es un caso concreto del concepto genérico de "número entero"). En el ejemplo suponemos que R1, Y1, R2, Y2 siguen siendo números fraccionarios tradicionales, los mismos que en el ejemplo anterior. Fíjese también que la asignación x = (R1, Y1) supone que el compilador sabe que al ser x un complejo, el símbolo "=" no representa aquí la misma operación que en el caso de, por ejemplo, z = 6; también supone que el compilador sabe que significa exactamente la expresión (R1, Y1) a la derecha. Si ahora queremos calcular el resultado de la suma de los dos complejos y el valor de su módulo, solo tenemos que hacer: Cc z = x + y; float m = z.mod(); Observe que en la primera línea el operador "+" no es exactamente el mismo que cuando se utiliza con enteros (por ejemplo, en 3 + 5). Esto lo deduce el compilador, porque sabe que x e y son complejos (dentro de la nueva clase hemos definido la operación "suma" entre sus elementos y le hemos asignado el operador "+"). Por otra parte, en la segunda línea solo tenemos que mandarle al complejo z el mensaje mod() - calcula el módulo -; la sintaxis es la señalada: z.mod(). Ya sabemos que el resultado es un escalar y lo asignamos al número fraccionario m. Fíjese también que en ambas líneas el operador de asignación "=" no representa la misma operación. En la primera se trata de una asignación entre complejos; en la segunda es la tradicional asignación entre números fraccionarios. Esta capacidad "camaleónica" de los operadores es típica de la POO, y se conoce como sobrecarga [4] (veremos que también puede haber sobrecarga de funciones). El compilador sabe en cada momento que operador debe utilizar de toda la panoplia disponible aunque estén bajo el mismo símbolo; lo sabe por el contexto (por la naturaleza de los operandos involucrados).


En la POO es frecuente que métodos análogos de clases distintas se referencien utilizando las mismas etiquetas. Es decir: supongamos que tenemos tres clases: los Complejos, los Vectores y los Racionales. Puede ocurrir que, por ejemplo, el método para calcular el módulo se denomine mod() en las tres clases. Aunque el resultado sea un número racional positivo en los tres casos, naturalmente su operatoria es distinta si se aplica a racionales, vectores o matrices. Sin embargo, no existe posibilidad de confusión en el programa porque cuando apliquemos el método en cada caso concreto, el compilador sabe a cual de ellos nos estamos refiriendo en base a los objetos involucrados. Esta característica de la POO, da origen a lo que se llama polimorfismo. El nombre es bastante descriptivo de su significado, y la posibilidad resulta ser de gran utilidad, pues simplifica la lectura del código. Cada vez que encontremos una invocación al método mod tenemos una imagen mental del resultado con independencia del objeto concreto al que se aplique. Llegados a este punto, introduciremos algún vocabulario específico para adaptarnos a la terminología, algo especial, que utiliza la POO: En vez de "variables" como en la programación clásica, los datos contenidos en las clases (y en los objetos concretos) se denominan propiedades, de forma que en el ejemplo anterior, nos referiremos a las partes real R1 e imaginaria Y1 del complejo x como las propiedades R e Y de x (los complejos siguen teniendo una parte "real" y otra "imaginaria" incluso en la POO). Por su parte las operaciones de manipulación contenidas en las clases (bajo la forma de funciones) reciben el nombre de métodos. Siguiendo con el ejemplo anterior, en vez de referirnos a función mod(), que calcula el módulo, diremos que mod es un método de la clase de los complejos. En lenguajes como C++, que puede tener elementos de ambos mundos, de la programación tradicional y de la POO, poder utilizar indistintamente los vocablos "función" o "método" es una ventaja, ya que nos permite distinguir en cada caso a que nos estamos refiriendo, si a funciones tradicionales o a funciones pertenecientes a clases. En uno de los párrafos anteriores hemos dicho: "... estos objetos son casos concretos del nuevo tipo, la clase de los complejos..."; utilizando la terminología correcta diríamos: "... estos objetos son instancias de la clase de los complejos...". Como puede verse, instancia significa una sustanciación concreta de un concepto genérico (la clase). En el mismo sentido podríamos decir que el número 5 (un objeto), una instancia de la clase de los números enteros, que tiene a su vez determinados métodos; en este caso las conocidas operaciones aritméticas: suma, resta, multiplicación..., así como las operaciones de asignación, la comparación, etc. Siguiendo este criterio, la expresión: Cc x = (R1, Y1) no se dice: "declaración de x como objeto de la nueva clase". Es más correcto señalar: "x es una instancia de la


clase de los complejos". Al hecho de sustanciar un objeto concreto de una clase se le denomina instanciar dicho objeto. Esta operación (crear un objeto) está encomendada a un tipo especial de métodos denominados constructores. Siguiendo con nuestro ejemplo, podemos suponer que la expresión: Cc c1(1, 3); Crea un complejo c1 cuyas partes real e imaginaria valen respectivamente 1 y 3, y que este objeto ha sido creado por una función (método) de Cc cuya misión es precisamente crear objetos según ciertos parámetros que son pasados como argumentos de la función. En ocasiones, cuando se quiere distinguir entre los valores y funcionalidades genéricos (de las clases) y los concretos (de los objetos), se suelen emplear las expresiones: propiedades de clase y métodos de clase para las primeras, y propiedades de instancia y métodos de instancia para los segundos. Así mismo, cuando se quiere distinguir entre una función normal (de la programación tradicional) y una función perteneciente a una clase (un método de la clase), se suele emplear para esta última la expresión función miembro (se sobreentiende "función miembro_de_una_clase"). Como puede suponerse, el encapsulamiento, junto con la capacidad de sobrecarga, constituyen en si mismos una posibilidad de abstracción para los datos mayor que la que facilitaba la programación tradicional. Ya sí puede pensarse en un complejo como algo que tiene existencia "real" dentro de nuestro código ya que lo tratamos (manipulamos) como ente independiente con características propias; de forma parecida a la imagen que de él tenemos en nuestra mente. Con ser lo anterior un paso importante, sin embargo, las mejoras de la POO respecto de la programación clásica no terminan aquí. Los Lenguajes Orientados a Objeto como el C++, además de los tipos de datos tradicionales, incluyen algunas clases pre-construidas, pero debido al hecho de que la "construcción" de un nuevo tipo de variable (como la clase Cc de los complejos en el ejemplo anterior) requiere un cierto trabajo de programación inicial para diseñar la clase, se pensó que sería estupendo poder aprovechar el trabajo realizado en determinados diseños para, a partir de ellos, obtener otros nuevos de características más o menos parecidas. Se trataba en suma de intentar reutilizar el código dentro de lo posible; en el mismo sentido que lo hacemos cuando tenemos que escribir una carta circular y buscamos en el ordenador si hay alguna anterior parecida para utilizarla junto con el procesador de textos, como punto de comienzo para la nueva. Para esto se dotó al lenguaje de dos nuevas capacidades: la Herencia y la Composición.


Por herencia ("Inheritance") se entiende la capacidad de poder crear nuevas clases a partir de alguna anterior, de forma que las nuevas "heredan" las características de sus ancestros (propiedades y métodos). Se trata por tanto de la capacidad de crear nuevos tipos de datos a partir de los anteriores. Una característica especial de la herencia es que si se cambia el comportamiento de la clase antecesora (también llamada padre, base o super), también cambiará el comportamiento de las clases derivadas de ella (descendientes). Como puede deducirse fácilmente, la herencia establece lo que se llama una jerarquía de clases del mismo aspecto que el árbol genealógico de una familia. Se entiende también que estos conceptos representan niveles de abstracción que permiten acercar la programación a la realidad del mundo físico tal como lo concebimos. Por ejemplo, entendemos que un motor eléctrico deriva de la clase general de los motores, de la cual derivan también los de gasolina, diesel, vapor, etc. y que, sin dejar de ser motores, cada subclase tiene sus características peculiares. Por supuesto que no tendría ningún sentido utilizar la herencia para crear simplemente un clónico de la clase base. En realidad, como en el ejemplo de la carta circular, la herencia se emplea como un primer paso (partir de algo existente) para a continuación, perfilar los comportamientos o datos que queremos pulir en la nueva versión de la clase. Como lo que principalmente interesa al usuario de una clase es su interfaz, son justamente algunos aspectos de esta interfaz los que se modifican, al objeto de adecuar la nueva subclase a las necesidades específicas del caso. Por lo general esta modificación de la interfaz se consigue de dos formas: • 1. Añadiendo propiedades y/o métodos que no existían en la clase base • 2. Sobrescribiendo propiedades del mismo nombre con distintos comportamientos (sobrecarga y/o polimorfismo). Aunque la herencia es uno de los pilares de la POO, tiene también sus inconvenientes. Por ejemplo, dado que el compilador debe imponer ciertas características en tiempo de compilación sobre las clases creadas por herencia, esto resulta en cierta rigidez posterior. Sin embargo, como hemos visto, una de sus ventajas es la reutilización del código. La composición (también llamada herencia múltiple) es la segunda vía para crear nuevas clases a partir de las existentes. Por composición se entiende la capacidad que presenta la POO de ensamblar un nuevo tipo (clase) cuyos elementos o piezas son otras clases. Es posible declarar clases derivadas de las existentes especificando que heredan los miembros de una o más clases antecesoras. Siguiendo con el símil de la carta circular, la composición equivaldría a escribirla reutilizando trozos de cartas anteriores. Es clásico el ejemplo de


señalar que podríamos crear una clase "coche" declarando que tiene un motor y cuatro ruedas, bastidor, aire acondicionado, etc, elementos estos pertenecientes a la clase de los motores, de las ruedas, los bastidores y los sistemas de climatización respectivamente. Este sistema tiene también sus ventajas e inconvenientes, pero es muy flexible, ya que incluso pueden cambiarse los componentes en tiempo de ejecución.

CONCEPTOS FUNDAMENTALES La programación orientada a objetos es una forma de programar que trata de encontrar una solución a estos problemas. Introduce nuevos conceptos, que superan y amplían conceptos antiguos ya conocidos. Entre ellos destacan los siguientes: •

Clase: definiciones de las propiedades y comportamiento de un tipo de objeto concreto. La instanciación es la lectura de estas definiciones y la creación de un objeto a partir de ellas.

Herencia: (por ejemplo, herencia de la clase D a la clase C) Es la facilidad mediante la cual la clase D hereda en ella cada uno de los atributos y operaciones de C, como si esos atributos y operaciones hubiesen sido definidos por la misma D. Por lo tanto, puede usar los mismos métodos y variables públicas declaradas en C. Los componentes registrados como "privados" (private) también se heredan, pero como no pertenecen a la clase, se mantienen escondidos al programador y sólo pueden ser accedidos a través de otros métodos públicos. Esto es así para mantener hegemónico el ideal de OOP.

Objeto: entidad provista de un conjunto de propiedades o atributos (datos) y de comportamiento o funcionalidad (métodos) los mismos que consecuentemente reaccionan a eventos. Se corresponde con los objetos reales del mundo que nos rodea, o a objetos internos del sistema (del programa). Es una instancia a una clase.

Método: Algoritmo asociado a un objeto (o a una clase de objetos), cuya ejecución se desencadena tras la recepción de un "mensaje". Desde el punto de vista del comportamiento, es lo que el objeto puede hacer. Un método puede producir un cambio en las propiedades del objeto, o la generación de un "evento" con un nuevo mensaje para otro objeto del sistema. Evento: Es un suceso en el sistema (tal como una interacción del usuario con la máquina, o un mensaje enviado por un objeto). El sistema maneja el evento enviando el mensaje adecuado al objeto pertinente. También se


puede definir como evento, a la reacción que puede desencadenar un objeto, es decir la acción que genera. •

Mensaje: una comunicación dirigida a un objeto, que le ordena que ejecute uno de sus métodos con ciertos parámetros asociados al evento que lo generó.

Propiedad o atributo: contenedor de un tipo de datos asociados a un objeto (o a una clase de objetos), que hace los datos visibles desde fuera del objeto y esto se define como sus características predeterminadas, y cuyo valor puede ser alterado por la ejecución de algún método.

Estado interno: es una variable que se declara privada, que puede ser únicamente accedida y alterada por un método del objeto, y que se utiliza para indicar distintas situaciones posibles para el objeto (o clase de objetos). No es visible al programador que maneja una instancia de la clase.

Componentes de un objeto: atributos, identidad, relaciones y métodos.

Identificación de un objeto: un objeto se representa por medio de una tabla o entidad que esté compuesta por sus atributos y funciones correspondientes.

En comparación con un lenguaje imperativo, una "variable", no es más que un contenedor interno del atributo del objeto o de un estado interno, así como la "función" es un procedimiento interno del método del objeto.

CARACTERÍSTICAS DE LA POO Existe un acuerdo acerca de qué características contempla la "orientación a objetos", las características siguientes son las más importantes: •

Abstracción: Denota las características esenciales de un objeto, donde se capturan sus comportamientos. Cada objeto en el sistema sirve como modelo de un "agente" abstracto que puede realizar trabajo, informar y cambiar su estado, y "comunicarse" con otros objetos en el sistema sin revelar cómo se implementan estas características. Los procesos, las funciones o los métodos pueden también ser abstraídos y cuando lo están, una variedad de técnicas son requeridas para ampliar una abstracción.


Encapsulamiento: Significa reunir a todos los elementos que pueden considerarse pertenecientes a una misma entidad, al mismo nivel de abstracción. Esto permite aumentar la cohesión de los componentes del sistema. Algunos autores confunden este concepto con el principio de ocultación, principalmente porque se suelen emplear conjuntamente.

Principio de ocultación: Cada objeto está aislado del exterior, es un módulo natural, y cada tipo de objeto expone una interfaz a otros objetos que específica cómo pueden interactuar con los objetos de la clase. El aislamiento protege a las propiedades de un objeto contra su modificación por quien no tenga derecho a acceder a ellas, solamente los propios métodos internos del objeto pueden acceder a su estado. Esto asegura que otros objetos no pueden cambiar el estado interno de un objeto de maneras inesperadas, eliminando efectos secundarios e interacciones inesperadas. Algunos lenguajes relajan esto, permitiendo un acceso directo a los datos internos del objeto de una manera controlada y limitando el grado de abstracción. La aplicación entera se reduce a un agregado o rompecabezas de objetos.

Polimorfismo: comportamientos diferentes, asociados a objetos distintos, pueden compartir el mismo nombre, al llamarlos por ese nombre se utilizará el comportamiento correspondiente al objeto que se esté usando. O dicho de otro modo, las referencias y las colecciones de objetos pueden contener objetos de diferentes tipos, y la invocación de un comportamiento en una referencia producirá el comportamiento correcto para el tipo real del objeto referenciado. Cuando esto ocurre en "tiempo de ejecución", esta última característica se llama asignación tardía o asignación dinámica. Algunos lenguajes proporcionan medios más estáticos (en "tiempo de compilación") de polimorfismo, tales como las plantillas y la sobrecarga de operadores de C++.

Herencia: las clases no están aisladas, sino que se relacionan entre sí, formando una jerarquía de clasificación. Los objetos heredan las propiedades y el comportamiento de todas las clases a las que pertenecen. La herencia organiza y facilita el polimorfismo y el encapsulamiento permitiendo a los objetos ser definidos y creados como tipos especializados de objetos preexistentes. Estos pueden compartir (y extender) su comportamiento sin tener que volver a implementarlo. Esto suele hacerse habitualmente agrupando los objetos en clases y estas en árboles o enrejados que reflejan un comportamiento común. Cuando un objeto hereda de más de una clase se dice que hay herencia múltiple.


Recolección de basura: la Recolección de basura o Garbage Collector es la técnica por la cual el ambiente de Objetos se encarga de destruir automáticamente, y por tanto desasignar de la memoria, los Objetos que hayan quedado sin ninguna referencia a ellos. Esto significa que el programador no debe preocuparse por la asignación o liberación de memoria, ya que el entorno la asignará al crear un nuevo Objeto y la liberará cuando nadie lo esté usando. En la mayoría de los lenguajes híbridos que se extendieron para soportar el Paradigma de Programación Orientada a Objetos como C++ u Object Pascal, esta característica no existe y la memoria debe desasignarse manualmente.


Turn static files into dynamic content formats.

Create a flipbook
Issuu converts static files into: digital portfolios, online yearbooks, online catalogs, digital photo albums and more. Sign up and create your flipbook.