obligatorio 2006

35
Notas sobre Orientación a Objetos Página 1 de 35 Ing. Jorge Corral NOTAS SOBRE ORIENTACIÓN A OBJETOS (CON EJEMPLOS EN C#) Versión Marzo de 2006 Este documento contiene notas y comentarios sobre orientación a objetos, tomando como lenguaje de ejemplos C#. Su objetivo no es sustituir las clases sino complementarlas. El alumno deberá completar lo aquí expuesto con notas propias tomadas de las clases, de los libros sugeridos como bibliografía, de los links dados y del resto del material publicado en www.espaciobios.com No es la idea leer estas notas para aprender por primera vez, sino leerlas luego de que sea visto el tema en clase. El motivo es que estas notas no presentan mucha introducción ni contexto, sino que van directamente al punto que se quiere transmitir; no presentan ejemplos de código e incluso su orden no es estrictamente el más adecuado. Por todo esto se requiere del lector tener un mínimo conocimiento previo sobre los temas aquí expuestos. Estas notas no tienen porque leerse secuencialmente (en orden) sino que puede consultarse directamente el tema que se quiera de los nueve presentados. Las definiciones no pretenden ser rigurosas. Lo importante no es ser capaz de escribir exactamente (o de memoria) la definición de un concepto, sino entender el concepto, su funcionamiento en C#, las implicancias, como utilizarlo y como codificarlo. El lector podrá comprobar muy fácilmente que las diferentes bibliografías dan diferentes definiciones sobre un mismo concepto. Por sugerencias, errores o comentarios sobre estas notas, favor de contactarse a la dirección [email protected] Contenido: Definiciones y Conceptos Básicos Pág. 2 Tipos de Datos, Referencias y Pasaje de Parámetros Pág. 7 Atributos y Métodos Estáticos Pág. 10 Herencia Pág. 13 Clases y Operaciones Abstractas Pág. 17 Creación de Objetos Pág. 19 Polimorfismo y Conceptos Relacionados Pág. 24 Interfaces Pág. 29 Asociaciones entre Clases Pág. 32

Upload: juan-luis-mazzaccaro

Post on 08-Dec-2015

222 views

Category:

Documents


0 download

DESCRIPTION

Analisis de sistemas

TRANSCRIPT

Page 1: Obligatorio 2006

Notas sobre Orientación a Objetos Página 1 de 35 Ing. Jorge Corral

NOTAS SOBRE ORIENTACIÓN A OBJETOS (CON EJEMPLOS EN C#)

Versión Marzo de 2006

• Este documento contiene notas y comentarios sobre orientación a objetos, tomando

como lenguaje de ejemplos C#. • Su objetivo no es sustituir las clases sino complementarlas. • El alumno deberá completar lo aquí expuesto con notas propias tomadas de las

clases, de los libros sugeridos como bibliografía, de los links dados y del resto del material publicado en www.espaciobios.com No es la idea leer estas notas para aprender por primera vez, sino leerlas luego de que sea visto el tema en clase. El motivo es que estas notas no presentan mucha introducción ni contexto, sino que van directamente al punto que se quiere transmitir; no presentan ejemplos de código e incluso su orden no es estrictamente el más adecuado. Por todo esto se requiere del lector tener un mínimo conocimiento previo sobre los temas aquí expuestos.

• Estas notas no tienen porque leerse secuencialmente (en orden) sino que puede consultarse directamente el tema que se quiera de los nueve presentados.

• Las definiciones no pretenden ser rigurosas. Lo importante no es ser capaz de escribir exactamente (o de memoria) la definición de un concepto, sino entender el concepto, su funcionamiento en C#, las implicancias, como utilizarlo y como codificarlo. El lector podrá comprobar muy fácilmente que las diferentes bibliografías dan diferentes definiciones sobre un mismo concepto.

• Por sugerencias, errores o comentarios sobre estas notas, favor de contactarse a la dirección [email protected]

Contenido: Definiciones y Conceptos Básicos Pág. 2 Tipos de Datos, Referencias y Pasaje de Parámetros Pág. 7 Atributos y Métodos Estáticos Pág. 10 Herencia Pág. 13 Clases y Operaciones Abstractas Pág. 17 Creación de Objetos Pág. 19 Polimorfismo y Conceptos Relacionados Pág. 24 Interfaces Pág. 29 Asociaciones entre Clases Pág. 32

Page 2: Obligatorio 2006

Notas sobre Orientación a Objetos Página 2 de 35 Ing. Jorge Corral

DEFINICIONES Y CONCEPTOS BÁSICOS

• Definición de Objeto: Un objeto es una entidad (una "cosa", por ahora) que "vive" en memoria, que tiene una identidad propia, que guarda valores y brinda una forma de acceder y modificar esos valores. Dicho más formalmente, un objeto es una entidad con identidad, estado y comportamiento. Estos tres conceptos serán explicados en próximos puntos.

• Definición de Clase: Una clase describe un conjunto de objetos, los que son hechos a imagen y semejanza de su clase. Las clases es lo que codificamos en C#. Como convención, los nombres de las clases siempre comienzan con letra mayúscula, por lo que "persona" no es un buen nombre de clase, mientras que "Persona" si lo es.

• Relación entre Clase y Objeto: Un objeto es un caso concreto de una clase. La mejor definición para objeto es: Un objeto es una instancia de una clase. La clase es lo que codificamos en C#, mientras que los objetos se crearan en memoria mientras se ejecuta el programa, tomando como "plantilla" o como "plano" su clase correspondiente. Cada objeto es de cierta clase. Es importante ver que ambos conceptos nunca conviven al mismo tiempo: la clase se codifica cuando se escribe el programa, mientras que los objetos existen cuando corremos (ejecutamos) el programa. El primer momento se llama Tiempo de Diseño, y el segundo Tiempo de Ejecución (design-time y run-time en inglés). Todos los objetos que sean instancias de la misma clase, son verdaderamente objetos del mismo tipo, es decir, son todos parecidos, por eso una clase se llama “clase”, porque define a una "clase de objetos". Como una "clase de alumnos" es un conjunto de personas en un salón con muchas características similares, podemos pensar que tenemos codificada en C# la clase Alumno que permite guardar nombre y edad, y por otro lado, cuando ejecutemos el programa, tendremos 20 objetos alumno, todos con nombre y edad, pero cada uno con su propio valor de nombre y edad. Mientras que la clase es una sola, los objetos instancias de esa clase serán muchos. Es la idea de la programación orientada a objetos la de instanciar muchos objetos a partir de la misma clase.

• Definición de Identidad: La identidad es una característica de todos los objetos. Todos los objetos son capaces de diferenciarse entre sí gracias a esa identidad. Dos objetos son diferentes aún cuando parezcan exactamente iguales. La identidad de un objeto esta dada automáticamente, no hay que hacer ni programar nada. Solo hay que saber de su existencia y como utilizarla. En los sucesivos puntos se irá aclarando este concepto.

• Definición de Atributo: Un atributo es una variable definida dentro de una clase. Por ejemplo, si deseamos que cada alumno tenga un nombre, entonces agregamos el atributo nombre de tipo string dentro de la clase Alumno . De esa manera, todos los objetos instancia de la clase Alumno tendrán su propio nombre.

• Definición de Operación: Una operación es una especificación de una función que se le pedirá a un objeto que ejecute. Notar el énfasis en la palabra especificación. Una especificación solo especifica (valga la redundancia), esto es que dice el QUE pero no el COMO. Entonces, una operación no es más que el encabezado de una función, que dice el nombre, lo que devuelve y una lista de parámetros.

Page 3: Obligatorio 2006

Notas sobre Orientación a Objetos Página 3 de 35 Ing. Jorge Corral

• Definición de Método: Es la implementación de una operación. Es el cuerpo de la función, el código útil que dice exactamente COMO vamos a llevar a cabo la operación. Es todo el código entre los corchetes de inicio y de fin de la función. Es el método que hay que seguir para llevar adelanta la operación, de allí su nombre.

• Aclaración: En computación, se habla frecuentemente de los conceptos "especificación" e "implementación". La especificación de algo dice QUE es lo que ese algo hace, mientras que la implementación de ese algo dice COMO lo va a hacer.

• Definición de Estado: El estado de un objeto es el conjunto de los valores de los atributos en un instante en el tiempo. Por ejemplo, ahora el estado de un objeto es: "Juan Perez", 20 años, pero mañana el estado del mismo objeto puede ser: "Juan Perez", 21 años, ya que actualizamos su edad. El estado de un objeto puede cambiar, pero sigue siendo el mismo objeto ya que su identidad no cambiará. Es el equivalente a tomar una fotografía de los valores de los atributos en un momento. De esto se desprende que el estado de un objeto se implementa a través de los atributos de su clase, y que el estado es algo dinámico que cambia con el tiempo.

• Definición de Comportamiento: El comportamiento de un objeto es el conjunto de operaciones que el objeto puede ejecutar. Esta dado entonces por las operaciones definidas en su clase. Generalmente las operaciones servirán para consultar el estado del objeto o para modificarlo, pudiendo también efectuar acciones sobre otros objetos.

• Definición de Referencia: Una referencia es una variable cuyo tipo de datos es una clase (o una interfaz). Es decir que la variable int i no es una referencia, mientras que Persona p sí lo es. Otra forma de verlo es que la referencia es una variable apunta a un objeto, mientras que una variable almacena un valor. Tener presente que no es lo mismo referencia que objeto: la referencia apunta al objeto.

• Diferencia entre Referencia y Objeto: Un objeto es una encapsulación de estado y comportamiento (atributos y operaciones), que ocupa un lugar en la memoria, mientras que una referencia es una variable que dice donde esta ubicado el objeto en esa memoria. Suelen confundirse los conceptos, diciendo cosas tales como "...el objeto "p" de tipo Persona..." lo cual siendo estrictos es incorrecto. Se debería decir "...la referencia "p" a un objeto de tipo Persona...". Los objetos no tienen nombre, las referencias si, ya que son variables, y las variables tienen nombre.

• ¿Que guarda una referencia?: Resulta interesante esta pregunta, ya que si una referencia es una variable, y las variables guardan un valor, entonces ¿que valor guarda una referencia? No parece guardar un entero, un booleano o cualquiera de los tipos de datos conocidos, pues debe “apuntar” a un objeto. Entonces ¿que quiere decir exactamente “apuntar”? Quiere decir guardar la dirección de memoria del objeto. Por lo tanto las referencias son variables que guardan la dirección en memoria que ocupa un objeto.

• Formas de obtener una Referencia: Una referencia puede apuntar a un objeto, por ejemplo mediante p = new Persona() o mediante p = q (siendo q otra referencia) o mediante p = operación() (asignar en "p" lo devuelto por una operación) o también puede no apuntar a ningún objeto, lo que se conoce como referencia nula (o vacía).

Page 4: Obligatorio 2006

Notas sobre Orientación a Objetos Página 4 de 35 Ing. Jorge Corral

• Referencia Nula: Cuando una referencia no apunta a ningún objeto, se dice que es una referencia nula, o una referencia a null . Esta palabra (null ) es una palabra reservada en C# que indica esta situación y además permite colocar una referencia en null . Esto no es ni un problema ni un error, es algo totalmente válido. Es como si dijéramos que tenemos un trozo de papel en donde vamos a anotar una dirección, pero que aún no la hemos escrito. Entonces, el papel esta vació, pero tiene lugar suficiente como para escribir una dirección. Para saber si una referencia no apunta a ningún objeto utilizamos el operador == de la siguiente manera: p == null . Este operador devuelve true si "p" no apunta a nadie (si es un “papel vació”) y false si apunta a alguien (si el “papel tiene escrita una dirección”). Cuidado: son dos signos de igual juntos, no uno solo.

• Igualdad de Referencias: Dado que una referencia guarda la dirección de memoria de un objeto, para que dos referencias "p" y "q" sean iguales deben apuntar al mismo objeto, es decir que p==q será true solamente cuando exista un solo objeto en memoria que sea apuntado tanto por "p" como por "q". En esta situación se dice que "p" es un alias de "q", ya que en realidad apuntan al mismo lugar de memoria.

• Mas sobre la Identidad de un Objeto: Ahora que sabemos lo que es un objeto y lo que es el estado de un objeto, podemos decir que aunque dos objetos tengan el mismo estado, serán dos objetos distintos, dado que cada uno tiene su propia identidad. Incluso puede ocurrir que, a los ojos del programador, dos objetos luzcan iguales: imaginemos dos objetos de tipo Alumno con el mismo nombre, misma edad e iguales valores en todos sus atributos. De todas maneras, si se crearon dos objetos distintos, la identidad será distinta, sin importar que luzcan iguales. En este caso son dos objetos que tienen los mismos valores para todos su atributos, es decir que estamos duplicando los valores. Cada objeto ocupará un lugar diferente de memoria, entonces aunque parezcan iguales, son dos cosas distintas. Justamente por eso es que se suele decir que la identidad de un objeto esta dada por la dirección de memoria que este ocupa. Por lo tanto, se ve como la dirección de memoria del objeto es utilizada, por un lado, por las referencias y por otro, para garantizar la identidad de un objeto.

Page 5: Obligatorio 2006

Notas sobre Orientación a Objetos Página 5 de 35 Ing. Jorge Corral

• Comentario AVANZADO: La memoria RAM utilizada por un programa que esta ejecutando, esta dividida en dos partes: el Stack y el Heap. El stack es traducido como la Pila, y el heap es traducido como el Montículo. En el stack se guardan todas las variables definidas en un bloque de código (todo lo que este entre dos llaves). Las variables sabemos que ocupan un lugar en memoria, y ahora afirmamos que es en memoria del stack. Cuando declaramos las variables (por ejemplo int i ) se le asigna inmediatamente un espacio en el stack. Cuando se llega a la llave de cierre (el símbolo "}") del bloque dentro del cual fue declarada la variable, la memoria del stack ocupada por la variable es devuelta al Sistema Operativo. Analizaremos ahora el caso de las referencias. Una referencia también es una variable, por lo que las mismas se alojan en el stack. Pero, recordar que una referencia apunta a un objeto, también en memoria. La diferencia es que los objetos se guardan en el heap, no en el stack. Por lo tanto, tenemos por un lado, a los objetos “viviendo” en el heap, y por otro a las referencias “viviendo” en el stack (junto a las variables), por lo que las referencias hacen que en el stack se guarden punteros al heap. Esto podría ser solamente una curiosidad, o algo creado por los docentes para complicar al alumno! Pero entender como se almacenan las referencias y los objetos ayuda a entender como funciona, a bajo nivel, la teoría de objetos. Una consecuencia importantísima de que los objetos vivan en el heap, es que cuando se llega a la llave de cierre de un bloque, las referencias que fueron declaradas adentro son destruidas, como ya se dijo, pero no los objetos. Esto quiere decir que la referencia se destruye, devolviendo la memoria al SO, pero el objeto al cual apuntaba sigue ocupando memoria en el heap. Entonces, la pregunta es: ¿Esta bien que esto suceda? ¿No debería también borrarse el objeto del heap? Primero que nada, así es como funciona C#, por lo que hay que vivir con ello. Segundo, sobre si debería o no borrarse el objeto del heap, la respuesta depende de si hay otras referencias que apunten al objeto (otros alias). Si los hubiere, entonces es claro que no se debería borrar, pero si no existen más referencias entonces tenemos un objeto que nadie lo apunta, por lo que nadie puede acceder a el! Es decir, que en este caso si sería deseable que el mismo se destruyera del heap. El programador nunca debe preocuparse por destruir los objetos del heap, ellos son destruidos automáticamente por un programa llamado Garbage Collector (recolector de basura). El GC es un programa que corre dentro de la CLR (Common Language Runtime, una parte de .NET) y lo único que hace es recorrer todo el heap, objeto por objeto, y fijarse si hay referencias a ellos. Si hay, los deja en donde están, pero si hay alguno que no tenga referencias, lo borra automáticamente. Entonces, podemos estar tranquilos que el GC devolverá la memoria que sabemos es "basura" (por eso lo de garbage), pues nadie puede utilizar un objeto que no tenga referencias, ya que sería como tener una casa, en una dirección, pero que nadie la sepa, entonces nadie podría llegar hasta ella.

• Acceder a Atributos e Invocar Métodos de un Objeto: Para acceder a un atributo o a un método de un objeto, basta con escribir una referencia al mismo seguida de un punto, seguido del nombre del atributo a acceder o método a invocar. Así si por ejemplo edad es un atributo de la clase Persona y aumentarEdad() es un método de esa misma clase, y teniendo una referencia “p” a un objeto de esa clase, para acceder al atributo se escribe: p.edad y para invocar al método se escribe: p.aumentarEdad() suponiendo que el método aumenta en uno la edad, devolviendo void . Tener presente que cuando se accede a un atributo, se devuelve el valor actual del mismo, por lo que éste debe ser colocado o bien dentro de una expresión, o bien asignado a una variable.

Page 6: Obligatorio 2006

Notas sobre Orientación a Objetos Página 6 de 35 Ing. Jorge Corral

• Operaciones públicas y privadas: En C#, cuando definimos una operación, debemos decir si es pública o privada (mediante las palabras reservadas public o private ). Una operación pública es visible (accesible) por cualquier clase, mientras que una privada es visible solo dentro de su propia clase. Aquí el término “visible” significa si podemos o no llamar a la operación desde otra clase diferente a la cual la contiene. Si una operación es declarada como privada, podrá ser llamada (invocada) solamente dentro de la clase que la contiene. Ni siquiera podrá ser llamada desde las clases heredadas (la herencia se explicará luego). Lo mismo ocurre con los atributos públicos y privados, que se explica a continuación.

• Atributos públicos y privados: Al igual que con las operaciones, un atributo puede ser tanto público como privado. También al igual que con las operaciones, si es privado solo es accesible dentro de su propia clase, y si es público desde cualquier lado. No se aconseja tener atributos públicos, siendo preferible que todos sean privados y brindar un par de métodos que permitan devolver el valor (getXXX(…) ) y modificar el valor (setXXX(…) ). El motivo de esto es que si hacemos que un atributo sea público, desde cualquier otra clase pueden modificar el valor de ese atributo sin que nosotros (la clase que contiene al atributo) podamos impedirlo ni controlarlo. Por ejemplo, si hacemos que el atributo edad sea publico, otro programador (seguramente por error) podría hacer p.edad = -1 y nosotros no podríamos impedirlo! Sin embargo, si lo hacemos privado y permitimos modificar la edad a través de un método (public void setEdad(int nuevaEdad) ), adentro de este método podemos colocar un if para verificar que la edad nueva sea positiva (mayor que cero). En caso de que no lo sea, tenemos el poder de decidir si de todas maneras la guardamos, o si dejamos el valor como estaba. Por tanto, como se dijo, es preferible que los atributos sean privados y brindar métodos de acceso y modificación para los mismos.

• Comentario AVANZADO: Este comentario ha sido clasificado como avanzado pues solamente le servirá a aquellas personas que ya hayan programado en un lenguaje estructurado, y marca una de sus diferencias con los lenguajes orientados a objetos. El comentario esta relacionado con el comentario anterior de como acceder a atributos e invocar métodos de un objeto. Lo interesante es notar que siempre partimos de la referencia al objeto, seguida del punto y finalmente del nombre del atributo o método que deseamos. Esto muestra que en los lenguajes orientados a objetos, justamente es el objeto, a través de su referencia, el centro del lenguaje. En un lenguaje estructurado las funciones se invocan en forma independiente y las variables también, a lo sumo dentro de una estructura de datos más grande. Ahora las funciones (métodos) y variables (atributos) siempre deben estar dentro de una clase, para acceder a ellas a través de las referencias a objetos de dichas clases.

Page 7: Obligatorio 2006

Notas sobre Orientación a Objetos Página 7 de 35 Ing. Jorge Corral

TIPOS DE DATOS, REFERENCIAS Y PASAJE DE PARÁMETROS

• Definición de Variable: Una variable consiste en: un nombre (por ejemplo “edad”), un tipo de datos (por ejemplo int ), un alcance (desde que se declara hasta el fin del bloque en donde fue declarada) y un valor (por ejemplo el número 4). Todas las variables son de algún Tipo de Dato. En C# existen dos grandes categorías de Tipos de Datos: los primitivos y los no primitivos, o también llamados los Tipos por Valor y los Tipos por Referencia, respectivamente.

• Definición de Tipos por Valor: Los tipos de datos primitivos o por Valor (también llamados atómicos) de C# son varios, entre los que están: bool , char , byte , short , int , long , float y double . Estos tipos primitivos guardan el valor dentro de la propia variable. Por esto son llamados Tipos por Valor.

• Definición de Tipos por Referencia: Los tipos de datos no primitivos son, principalmente, los objetos y los arrays. Estos tipos no primitivos son también llamados Tipos por Referencia, ya que la variable no guarda su valor, sino que guarda una referencia al objeto o array. Cuando declaramos una nueva clase estamos declarando también un nuevo tipo de datos y sus instancias tendrán como tipo de dato a su clase. Por ejemplo, la referencia "p" a un objeto instancia de la clase Persona es declarada como Persona p de la misma manera que declaramos int i para decir que "i" tiene como tipo de dato a los números enteros.

• Comentario: Una consecuencia de que los objetos (y arrays) sean manejados por referencia, es que dos variables pueden referenciar al mismo objeto (o array), caso ya visto y conocido como alias. Esto no puede ocurrir con los tipos primitivos, pues cada variable aloja su propio valor y no la dirección del mismo. Por ejemplo, si escribimos: Persona p, q; p = new Persona(); q = p; ...lo que estamos haciendo es que tanto “p” como “q” apunten al mismo objeto de tipo Persona . O sea, que en realidad no estamos copiando nada, porque el objeto sigue en el mismo lugar de memoria. Para ser más exactos, sí estamos copiando algo: la dirección de memoria del objeto; pero nunca el objeto mismo. Sin embargo, si escribimos: int i, j; i = 2; j = i; ...lo que estamos haciendo es copiar el valor de "i" en "j", por lo que "j" guarda otra copia del valor 2.

Page 8: Obligatorio 2006

Notas sobre Orientación a Objetos Página 8 de 35 Ing. Jorge Corral

• Pasaje de parámetros: Cuando pasamos parámetros a un método, por defecto, el pasaje se hace por valor. Esto quiere decir que, una vez adentro del método, lo que tenemos son copias de los valores pasados originalmente al momento de invocar al método. Este es el comportamiento por defecto, que puede cambiarse. Entonces, si invocamos a un método pasándole la variable "x" de tipo int con un valor de 10 y en el encabezado del método declaramos como parámetro de entrada a "z" (por supuesto que también de tipo int ), entonces el parámetro "z" recibirá una copia del valor 10. Esto hace que hayan dos valores 10. Dentro del método, aunque modifiquemos el valor de "z", por ejemplo le sumamos uno (para que valga 11), ese nuevo valor solamente es válido dentro del método. Una vez que el método termine (cuando se llega al corchete final) y volvamos al método que nos había llamado, el valor de "x" seguirá siendo 10. Esto puede sonar obvio, ya que se trataban de dos variables distintas! Una es "x" y la otra es "z", se llaman distinto, entonces son diferentes. Pero, incluso cuando las llamemos igual, por ejemplo si ambas fueran llamadas "x", de todos modos serían dos variables diferentes! (ahora ya no es tan obvio, no?) Entonces, para el caso por defecto, lo que parece obvio es cierto! Pero, dijimos que este comportamiento es por defecto, y puede ser modificado. Podemos hacer que ambas variables compartan el mismo valor. Para lograr esto, lo que tenemos que hacer es que el parámetro dentro del método ("z") no guarde un nuevo valor, sino que apunte al mismo valor que "x". Si esto le hace recordar a las referencias, va por buen camino. Lo que podemos hacer, es que "z" en realidad guarde la dirección de memoria de "x" de manera que sea un alias de "x". Por lo tanto, si modificamos el valor de "z" (sumando 1) dentro del método, entonces tanto dentro como fuera del método, el valor de "x" será 11. Es decir que el efecto de haberle sumado 1 a "z" adentro del método, persiste aun después que el método haya terminado. Para lograr esto, usamos la palabra clave ref dos veces: cuando invocamos al método, escribimos metodo(ref x ) y adentro del método escribimos: public void metodo(ref int z) {...codigo...}

• Comentario AVANZADO: Cuando pasamos una referencia a un objeto como parámetro de un método, esa referencia es pasada por valor al método. Esto quiere decir que el método podrá acceder y modificar el contenido del objeto (porque tiene su dirección) pero no podrá modificar referencia (es decir, no puede hacer que la referencia que le pasaron apunte a otra cosa). Es más, en Java por ejemplo, todos los pasajes de parámetros son por valor. Esto no es así en C#, donde tenemos pasaje de parámetros por valor (que es el defecto), por referencia (ref ) e incluso de sólo salida (out ). En resumen, cuando pasamos referencias por valor (sin poner ni ref ni out ) lo que el método recibe es una copia de la dirección de memoria del objeto, por eso es que lo puede modificar, porque sabe en donde está, y por eso también es que no puede modificar su ubicación, porque el método tiene una copia de la dirección del objeto, no la dirección original.

Page 9: Obligatorio 2006

Notas sobre Orientación a Objetos Página 9 de 35 Ing. Jorge Corral

• Comentario AVANZADO II: De lo anterior se desprende que el siguiente código no cumple su objetivo (cambiar de lugar entre sí los objetos pasados por parámetro): public void intercambiar(Persona a, Persona b) { Persona aux; aux = a; a = b; b = aux; } ...es decir que si lo invocamos con dos objetos p1 y p2, este procedimiento no tiene ningún efecto. El motivo es que como "a" y "b" contienen copias de las direcciones de p1 y p2 respectivamente, lo que estamos haciendo es intercambiarlos solamente dentro del método, pero una vez que el método se termine, las direcciones de p1 y p2 serán las mismas de antes, ya que las asignaciones hechas adentro del método tienen como alcance solamente al propio método, y su efecto no perdura fuera del método. Para que verdaderamente tenga efecto, debemos colocar la palabra reservada ref antes de invocar al método y dentro de los parámetros del método.

Page 10: Obligatorio 2006

Notas sobre Orientación a Objetos Página 10 de 35 Ing. Jorge Corral

ATRIBUTOS Y MÉTODOS ESTÁTICOS

• Repaso: Dijimos que un atributo es una variable dentro de una clase, donde cada objeto (instancia de esa clase) le asigna un valor. Por lo tanto, podemos tener 100 objetos alumno cada uno con su nombre, siendo nombre un atributo de tipo string de la clase Alumno . Otra forma de llamarlos es "atributo de instancia", ya que cada instancia le asocia un valor, o lo que es lo mismo, el “dueño” de cada valor es la instancia.

• Definición de Atributo Estático: Un atributo estático es aquel cuyo valor es compartido por todas las instancias de la clase. En contraposición a un atributo de instancia, un atributo estático tiene un único valor, el cual es compartido por todas las instancias. El “dueño” de ese valor es la clase. Por esto es que otra forma de llamarlos es "atributo de clase". Otra diferencia entre ambos tipos de atributos, es que mientras que cada vez que instanciamos una clase estamos creando un nuevo valor para cada atributo de instancia, esto no ocurre con los atributos estáticos. Siempre hay un solo valor, el cual es visto y compartido por todas las instancias. Por ejemplo, supongamos que el sueldo de todos los empleados es el mismo y aunque varíe, siempre ganarán todos lo mismo: en lugar de definir un atributo de instancia double sueldo , definimos un atributo estático static double sueldo . De esta manera nos evitamos tener muchas copias del mismo valor, y tenemos solo una copia del suelo de todos. Esto solamente se deberá hacer cuando todos los empleados cobran lo mismo (imagínelo por el bien del ejemplo).

• Consecuencia: Una consecuencia de lo anterior, y sobre todo teniendo en cuenta que un atributo estático es un atributo de la clase, y no de las instancias, es que podemos asignarle un valor aun cuando no hayamos creado ninguna instancia de la clase. Esto es imposible con los atributos de instancia. Por lo tanto, la existencia de los atributos estáticos es independiente de la existencia de objetos. Así, podemos utilizar el valor de un atributo estático (asignarle un valor, consultarlo, modificarlo, consultarlo de nuevo, etc.) aún cuando no hayamos creado ninguna instancia de esa clase.

• Definición de Método Estático: Dado que un atributo estático puede tener un valor aún antes de haber creado objetos, y podemos acceder y cambiar su valor, todo esto sin que exista ninguna referencia a ningún objeto, la pregunta que debería surgir es ¿Cómo hacemos para manipular los atributos estáticos? Porque es seguro que a través de los métodos no podemos, pues para invocar a un método tenemos que tener una referencia a un objeto (recordar la sintaxis: referencia.metodo() ), y dijimos que podemos usar los atributos estáticos independientemente de la existencia de objetos (por lo tanto, independientemente de la existencia de referencias). La solución entonces es mediante los llamados métodos estáticos, los cuales están pensados para permitir acceder y modificar los valores de los atributos estáticos, sin la necesidad de instanciar objetos.

• Sintaxis I (declarar atributos y métodos estáticos): Para declarar que un atributo o método es estático, basta con poner la palabra reservada static antes de su tipo. Por ejemplo: static int atribEstatico para declarar un atributo estático, y static int metodoEstatico() {...} para declarar un método estático.

Page 11: Obligatorio 2006

Notas sobre Orientación a Objetos Página 11 de 35 Ing. Jorge Corral

• Sintaxis II (utilizar atributos y métodos estáticos): Para acceder a un atributo o método estático, recordar que no es necesaria ninguna referencia a ningún objeto. Por lo tanto, se utiliza el nombre de la clase seguido del punto y seguido del nombre del atributo o método estático. Por ejemplo, siguiendo el ejemplo anterior, para acceder al atributo estático atribEstatico debemos escribir NombreClase.atribEstatico y para invocar al método estático NombreClase.metodoEstatico() .

• Error Común: Un error muy común entre quienes se introducen en la orientación a objetos, es pensar que un atributo estático no puede cambiar su valor. Suponemos que esta confusión se debe a la utilización de la palabra "estático" que en español se puede interpretar como "algo que no cambia". ESTO ES INCORRECTO. Los atributos que no pueden cambiar su valor con las constantes (declaradas mediante const ), y nada tienen que ver con los atributos estáticos explicados anteriormente. Por lo tanto, recordar siempre que un atributo estático PUEDE cambiar su valor.

• Regla (primera parte): Una regla muy importante a la hora de manipular atributos estáticos y de instancia, y de métodos estáticos y de instancia, es: Desde un método estático solo podemos acceder a atributos estáticos, y no a atributos de instancia. Es decir que desde un método estático no podemos acceder a nada de las instancias. Esto tiene un motivo: ¿Cómo vamos a poder acceder a atributos de instancia si tal vez no exista ninguna instancia?

• Regla (segunda parte): Desde un método de instancia se puede acceder a un atributo de clase, ya que éstos están diseñados para ser compartidos por todas las instancias. Por lo tanto, mientras que desde "el mundo de las instancias" podemos acceder al "mundo de lo estático", lo inverso no es posible.

• Comentario: Se dice también que todo lo que es estático "siempre existe", "siempre esta disponible", aún antes de crear ningún objeto. Por esto es que una vez que creamos un objeto, desde sus métodos de instancia podemos acceder a los atributos estáticos porque ya fueron creados.

• Pregunta (y respuesta): Entonces, si los atributos estáticos son creados antes que cualquier instancia, y siempre están disponibles, ¿Cuando se crean? Debería resultar fácil responder a la pregunta ¿Cuando se crean los atributos de instancias? (R: cuando creamos la instancia!). Pero, no parece tan fácil responder la otra pregunta. La respuesta es: Los atributos estáticos se crean ni bien ejecutamos el programa. Es de las primeras cosas que se crean cuando se corre un programa.

Page 12: Obligatorio 2006

Notas sobre Orientación a Objetos Página 12 de 35 Ing. Jorge Corral

• Concepto AVANZADO: No podemos dejar pasar la oportunidad, ya que estamos tratando el tema de atributos y métodos estáticos, de hablar del constructor estático. Para introducir el tema, pensemos que debería ser fácil contestar la pregunta: ¿Donde ponemos el código para inicializar los atributos de instancia? (R: en los constructores!). Pero no es tan fácil contestar la pregunta: ¿Donde ponemos el código necesario para inicializar los atributos estáticos? Una primera respuesta, fácil y sencilla, seria decir: en el mismo lugar en que los declaramos! O sea, ponemos static int atribEstatico = 999 Si bien es una respuesta correcta, es incompleta y simplista. ¿Que pasa si el atributo estático, en lugar de ser un entero es un array de enteros? No podemos poner una sentencia for en el mismo lugar que declaramos el atributo estático! Recordar que tampoco podemos inicializar los atributos estáticos en los constructores porque éstos son llamados solamente cuando creamos objetos, y los atributos estáticos tienen que ser independientes de la creación de objetos. La respuesta entonces es que necesitamos un nuevo tipo de constructor, especialmente pensado para inicializar los atributos estáticos. Ese es el papel del llamado constructor estático. Hay solamente un constructor estático, que recibe el mismo nombre que la clase, pero que no puede devolver nada, ni ser ni público ni privado, ni recibir nada. No tenemos porque usarlo, salvo que necesitemos inicializar un atributo estático que no sea solamente un numero o un string . Entonces, dentro de este constructor estático es que pondremos, por ejemplo, la sentencia for que inicializa el array estático de enteros. Debería ser obvio que dentro de este constructor estático, solamente podemos hacer referencia a los atributos estáticos. Después de todo, el constructor estático es un método estático, por lo tanto se cumplen las reglas anteriores. Si bien es un concepto no muy fácil de comprender, es muy importante, pues la función que cumple no se puede obtener de ninguna otra manera: no hay otra forma de inicializar atributos estáticos complejos; sólo usando el constructor estático. De todos modos, se lo considerara como un tema avanzado pues no es muy común utilizarlos.

Page 13: Obligatorio 2006

Notas sobre Orientación a Objetos Página 13 de 35 Ing. Jorge Corral

HERENCIA

• Definición de Herencia: La herencia es el mecanismo por el cual se permite a una clase heredar los atributos y métodos de otra, pudiendo utilizarlos como suyos sin la necesidad de copiar y pegar el código. Así, por ejemplo, si en la clase Empleado definimos los atributos string nombre e int edad y los métodos string getNombre(){...} e int getEdad(){...} y hacemos que la clase Asalariado herede de Empleado , entonces la clase Asalariado tendrá (heredara) ambos atributos y métodos "gratuitamente", sin tener que codificarlos nuevamente.

• Regla: Una clase solo puede heredar de una sola clase, no de varias (lo cual es llamado herencia múltiple en lenguajes como C++). Por tanto, C# no permite la herencia múltiple (ni tampoco ningún lenguaje basado en .NET). Java tampoco la permite, aunque C++ si. Por ejemplo, no podemos crear una clase Anfibio que herede de VehiculoTerrestre y de VehiculoAcuatico . Esto no quiere decir que una misma clase no pueda ser heredada hacia varias. Es decir, podemos hacer que tanto Asalariado como Jornalero , ambas hereden de Empleado . Esto no es herencia múltiple, ya que de una misma clase si pueden heredar varias.

• Objetivos de la Herencia: La herencia es una buena forma de modularizar código, es decir de factorizar código, poniéndolo junto en una clase y permitiéndolo utilizar (heredar) por otras clases. Esto permite no repetir código (ni copiar y pegar) y favorece la mantenibilidad del código, ya que cambiando en un solo lugar el código, esto repercute automáticamente en todas las heredadas. Si no se utiliza la herencia, cada cambio impactaría en muchos cambios, uno por cada clase que repite el atributo o método (cuanto menos código se escribe, menos errores habrá!).

• Definición de clase Base y clase Derivada: Dado que la herencia es una relación entre dos clases, la que brinda ("regala") su código y permite la herencia se la denomina clase Base, o Superclase. La que toma ("hereda") el código se la denomina clase Derivada o Subclase. En C# se suele utilizar el nombre de clase Base (o base class en ingles). Rescribiendo una de las reglas anteriores, se puede afirmar que una clase tendrá como máximo una clase base, pero podrá tener muchas clases derivadas.

•••• Sintaxis: La forma de especificar la herencia es que la clase derivada indique quien es su clase base. Esto se hace colocando dos puntos luego del nombre de la clase, seguidos del nombre de la clase base. Por ejemplo, si queremos que la clase Asalariado herede de la clase Empleado , al momento de codificar la clase Asalariado escribimos: public class Asalariado : Empleado { ...código de la clase Asalariado... }

• Regla: Toda clase hereda de otra, con la única excepción de la clase ya definida Object (definida dentro de .NET y también en Java). Esto quiere decir que si cuando codificamos una clase no decimos de quien deriva, entonces por defecto derivará (heredará) de la clase Object . Esta clase Object se explicara más en puntos siguientes.

Page 14: Obligatorio 2006

Notas sobre Orientación a Objetos Página 14 de 35 Ing. Jorge Corral

• Regla: La subclase heredará todos los atributos y métodos que no sean privados (private ). Los campos y métodos privados solo pueden ser utilizados dentro de la propia clase, ni siquiera en las clases derivadas. Otra forma de ver esto, es pensar que la subclase heredara todo, pero no podrá acceder a los atributos y métodos privados que ha heredado.

• Sintaxis II: Para hacer referencia a un campo o método de la clase base, simplemente se lo invoca. También se puede utilizar la palabra reservada base . Por ejemplo, desde Asalariado para invocar al método setNombre() de Empleado se puede escribir base.setNombre() . El uso de la palabra reservada base con paréntesis (:base(...) ) se explicara en puntos siguientes.

• Comentario AVANZADO: Sólo en un caso especial es necesario utilizar base para invocar un método de la clase base: cuando en la clase derivada se haya redefinido ese método, pero de todos modos se quiera invocar el método original de la clase base. Para entender esto es necesario leer el capítulo de Polimorfismo y Conceptos Relacionados.

• Cuidado: La introducción de la herencia en un programa orientado a objetos, si bien es muy común y muy útil, también introduce muchos puntos de los cuales se debe estar alerta. Estos sobre todo tienen que ver con la construcción de objetos, y serán tratados en el capítulo Construcción de Objetos. Por lo tanto, se debe pensar cuidadosamente antes de relacionar mediante herencia a dos clases y luego de hecho, pensar cuidadosamente en las consecuencias que esto traerá.

• Definición de Instancia Directa e Indirecta: La introducción de la herencia trae consigo la necesidad de reexaminar varias cosas, entre ellas el significado de ser "instancia de una clase". Un objeto es instancia directa de una clase si surge de hacer un new de esa clase (este es el caso típico). Un objeto es instancia indirecta de su clase base y de todas sus clases antecesoras en la jerarquía de clases, hasta llegar a la mismísima clase Object . Por lo tanto, un objeto de tipo Asalariado es instancia directa de la clase Asalariado y su vez instancia indirecta de la clase Empleado y también instancia indirecta de la clase Object .

• Regla: Si generalizamos el punto anterior, llegaremos a la conclusión de que todo objeto, sin importar de quien sea instancia directa, siempre será instancia indirecta de la clase Object . Esto quiere decir que todas las instancias son instancia indirecta de Object .

• Definición de Redefinición: Se dice que una operación redefine a otra operación de su clase base cuando se le asocia un nuevo método en la clase derivada, entre dos clases relacionadas entre si por medio de la herencia.

Page 15: Obligatorio 2006

Notas sobre Orientación a Objetos Página 15 de 35 Ing. Jorge Corral

• Objetivo de la Redefinición: Recordar que la herencia permite a una clase heredar atributos y operaciones de su clase base. Pero: ¿Qué sucede si la clase derivada (la que hereda) desea modificar el comportamiento de las operaciones que heredó? ¿Es posible esto? Y si lo es ¿Como? Por ejemplo, imaginemos que la clase Asalariado hereda de la clase Empleado , y esta ultima tiene una operación llamada calcularSueldo() que devuelve el sueldo del empleado. Imaginemos ahora que la clase Asalariado tiene otra forma de calcular el sueldo, ya que el sueldo de los asalariados se calcula distinto que el sueldo de los empleados, pero sin embargo desea utilizar el mismo nombre de la operación, ya que "calcularSueldo" es un nombre adecuado. Entonces, el problema esta en que heredó una operación con un método asociado, y ahora quiere utilizar la misma operación pero darle otro método distinto. Ahí es cuando se debe redefinir la operación heredada, cabiándole el método.

• Sintaxis de la Redefinición: Para redefinir una operación se utiliza la palabra reservada override en la clase derivada (en la clase que redefine la operación). Por otro lado, la clase base que desea permitir la redefinición de una operación suya, deberá colocar la palabra reservada virtual en cada operación que desea permitir redefinir.

• Formas de Redefinición (se utilizaran conceptos aún no explicados): En realidad, existen dos formas de redefinición, pero desafortunadamente C# utiliza la misma palabra reservada override . La primera forma es cuando la clase base tiene una operación virtual (public virtual oper() {...método...} ), y la subclase la redefine, dándole otro método. Este caso realmente es de re-definición, pues la operación ya estaba definida, ya tenía su método, y ahora lo redefinimos. La segunda forma es cuando la clase base tiene una operación abstracta (public abstract oper(); ) y la subclase la redefine, también con override . En este caso, más que una redefinición, es una primera definición, pues las operaciones abstractas no tienen método. De todos modos, en ambos casos se llama redefinición, aunque es importante reconocer la diferencia: en el primer caso ya existía un método para la operación, mientras que en el segundo no.

• Principio de Reemplazabilidad: Este principio o regla, llamado en ingles subsumption, dice lo siguiente: Dadas dos clases A y B, donde B es subclase de A, y dadas dos instancias: a de tipo A y b de tipo B, entonces b será también de tipo A. Usando ejemplos, este principio dice que un objeto asalariado es de tipo empleado, o que un objeto moto es de tipo vehículo, suponiendo que asalariado hereda de empleado, y que moto hereda de vehículo. Esto, aunque bastante obvio en la vida real, tiene varias implicancias importantes en la teoría de objetos. A continuación dos consecuencias.

• Consecuencia I: Si un método espera como parámetro un objeto de un cierto tipo, podemos pasar cualquier objeto que sea subclase del tipo esperado. Es decir, si esperamos un objeto de tipo Vehículo , podremos pasar como parámetro un objeto de tipo Moto ; si esperamos un Empleado , podemos pasar un Asalariado o un Jornalero . De ahí el nombre de dicho principio: reemplazabilidad.

Page 16: Obligatorio 2006

Notas sobre Orientación a Objetos Página 16 de 35 Ing. Jorge Corral

• Consecuencia II: Sabemos que una referencia debe ser definida del mismo tipo que los objetos a los que apuntará. Ahora agregaremos que puede ser definida como de un supertipo de aquellos a los que apuntará. Es decir, si tenemos una referencia "e" de tipo Empleado , podremos apuntar a cualquier objeto Empleado , pero también podremos apuntar a cualquier objeto Asalariado , ya que los asalariados son empleados, por el principio de Reemplazabilidad.

Page 17: Obligatorio 2006

Notas sobre Orientación a Objetos Página 17 de 35 Ing. Jorge Corral

CLASES Y OPERACIONES ABSTRACTAS

• Definición de Operación Abstracta: Una operación abstracta es una operación que no tiene método en la clase en la cual es definida. Es decir, que "no hace nada". Por lo tanto, no tiene cuerpo, no tiene código realmente útil, solo tiene el encabezado, indicando lo que devuelve, el nombre y la lista de parámetros. Una operación que no sea abstracta se dice concreta. Por lo tanto, todas las operaciones a las cuales nos referíamos en páginas anteriores son concretas, ya que asumíamos que tenían un método asociado a cada una de ellas.

• Objetivo: En principio no parece evidente porque quisiéramos tener una operación que no haga nada, sin método. Una de las respuestas será contestada en el capítulo sobre Polimorfismo y Conceptos Relacionados. Adelantándonos un poco, el motivo para hacer una operación abstracta, es que no sepamos, en esa clase, que método ponerle, pero que de todos modos queremos que la operación esté presente en esa clase.

• Idea: La idea es que una operación abstracta sea implementada (dado un método para ella) en las subclases. Por tanto, se puede decir que al declarar una operación como abstracta, estamos casi obligando a que las clases derivadas le asocien un método. El "casi" se explicará luego.

• Definición de Clase Abstracta: Una clase abstracta es una clase que no puede instanciarse, es decir que no le podemos hacer new. Siendo un poco más exactos, deberíamos decir que una clase es abstracta cuando no podemos obtener instancias directas de esa clase, pero si tendremos instancias indirectas de dicha clase: todas las que sean instancias directas de sus clases derivadas, y las derivadas de sus derivadas, etc., etc., etc. Las clases que no son abstractas se dicen clases concretas.

• Regla: Si una clase tiene (al menos) una operación abstracta, entonces la clase debe ser abstracta. Cuidado porque lo inverso no es cierto: una clase abstracta no tiene porque tener operaciones abstractas. Una clase puede ser abstracta "porque si", aun teniendo todas sus operaciones concretas. La regla entonces lo que implica es que si tenemos por lo menos una (o más) operaciones abstractas, entonces la clase debe ser abstracta. Si no lo es, el compilador nos dará un mensaje de error.

• Motivo de la Regla Anterior: Explicaremos ahora el motivo de la regla anterior. Supongamos (por absurdo, sabiendo que esta mal) que tenemos una clase concreta con una operación abstracta. Imaginemos ahora que hacemos un new de esa clase (ya que es concreta podemos hacerlo). Ahora imaginemos que le pedimos a ese objeto recién creado que ejecute la operación abstracta (por ejemplo e.operAbstracta(); ) ¿Que pasaría? ¿Que devolvería? ¿Que haría? Le estamos pidiendo al objeto que ejecute una operación que no tiene código!!! (recordar que la operación es abstracta!) Esto es absurdo, no tiene sentido, no haría nada, se colgaría el programa, por lo tanto el error lo cometimos suponiendo que podíamos tener una operación abstracta en una clase concreta. Es decir, no podemos permitir hacer un new de una clase que tenga alguna operación abstracta. Esto justifica la regla anterior.

• Cuidado: Clase abstracta y operación abstracta no son la misma cosa, ni siquiera parecidas. Una clase abstracta no puede instanciarse directamente, mientras que una operación abstracta no tiene método en la clase que la define. La única relación entre ambos conceptos es la regla recién explicada. No confundir los conceptos!!!

Page 18: Obligatorio 2006

Notas sobre Orientación a Objetos Página 18 de 35 Ing. Jorge Corral

• Regla (explicación del "casi" utilizado antes): Cuando hacemos una operación abstracta, la clase también debe ser abstracta por la regla explicada, y las clases derivadas (subclases) se ven enfrentadas al siguiente "dilema": si no redefinen la operación abstracta (dándole un método) entonces estarían heredando una operación abstracta, por lo cual ellas mismas también tienen que ser clases abstractas! Si no desean ser clases abstractas, entonces tienen que darle un método a todas las operaciones abstractas definidas en la clase base. Por eso se dijo que hacer una operación abstracta "casi" obliga a que las subclases la redefinan, porque pueden no hacerlo, pero al costo de que ellas mismas se deben volver abstractas.

• Comentario (sobre la precisión del lenguaje): La existencia de un concepto como el de operación abstracta hace ver la necesidad de utilizar un lenguaje lo más exacto posible. Por eso es que una operación no es lo mismo que un método. La definición de operación abstracta es un claro ejemplo de los beneficios de manejar conceptos individualmente y con la mayor precisión posible en usar los términos adecuados. Otro ejemplo de la necesidad de ser precisos surge de las definiciones de operación abstracta y de clase abstracta. En el caso de operación abstracta, es MUY importante notar que dice que la operación no tendrá método en la clase en la cual es definida. En las subclases, la misma operación podrá tener un método (por ejemplo, porque se redefinición la operación). En el caso de clase abstracta, es MUY importante notar que dice que de la clase no podemos obtener instancias directas, pero si podremos obtener instancias indirectas. En definitiva, ser preciso vale la pena, ya que ayuda a entender y diferenciar las sutilezas que tiene la teoría de objetos.

Page 19: Obligatorio 2006

Notas sobre Orientación a Objetos Página 19 de 35 Ing. Jorge Corral

CREACIÓN DE OBJETOS

• Definición de Constructor: Un constructor es un método. Es el primer método que se invoca cuando se crea un objeto. Es el que justifica la presencia de paréntesis curvos en la instanciación de una clase: Persona p = new Persona() ;

• Objetivo: El objetivo del constructor es inicializar la instancia que se está creando. Dado que es el primer método que se invoca sobre la instancia, es el lugar ideal para colocar cualquier código de inicialización. Por ejemplo, si deseamos que por defecto la edad de todas las personas sea de 18 años, colocaremos ese código dentro del constructor de la clase. El constructor puede inicializar los atributos de la instancia, pero también puede realizar otras tareas, como por ejemplo conectarse a una Base de Datos, escribir algo en un archivo, etc.

• Regla: Todos los constructores llevan el mismo nombre que la clase en la que están contenidos.

• Regla: Un constructor, si bien puede no recibir nada o recibir cualquier cantidad/tipo de parámetros, no puede devolver nada. Tan es así, que ni siquiera se puede poner void . No se pone nada como tipo de retorno del método.

• Definición de Constructor por Defecto: Si existe, es único. Es único porque no recibe ningún parámetro, mientras que los demás constructores reciben algún parámetro. Por ejemplo, en la clase Persona , el constructor por defecto se llamara igual que la clase y no recibirá ningún parámetro, por lo que se verá de la siguiente manera: public Persona() { ...código del constructor por defecto... }

• Regla: Si en una clase no se define ningún constructor, C# nos provee uno por defecto que lo único que hará será invocar al constructor por defecto de su superclase.

•••• Definición de Constructor Común: Si es que existen, no tienen porqué ser únicos. Reciben cualquier cantidad y tipo de parámetros, todos diferentes entre sí para saber exactamente a cual se desea invocar. Por ejemplo, podemos crear un objeto de tipo Persona ya con un nombre dado, utilizando un constructor común que reciba como parámetro un string con el nombre de la persona a crear. Dicho constructor será: public Persona(string nombre) { ...código del constructor común... }

• Motivo de la Existencia de Múltiples Constructores: El motivo de tener muchos constructores para una misma clase (donde serán muchos constructores comunes y solo uno por defecto) es permitir la creación de objetos de esa clase con diferentes cantidades de datos. Por ejemplo, si deseamos crear un objeto Persona y no tenemos ningún dato, entonces utilizaremos el constructor por defecto. Pero, si deseamos crear una persona y ya sabemos su nombre, entonces podemos utilizar el constructor común que reciba un string . así, cuanto mas constructores se coloquen en una clase, mas flexibilidad se esta dando para la construcción de instancias de esa clase.

Page 20: Obligatorio 2006

Notas sobre Orientación a Objetos Página 20 de 35 Ing. Jorge Corral

• Comentario AVANZADO: En realidad, y siendo más precisos, los constructores si devuelven algo: una referencia al nuevo objeto creado. Esto es, la dirección de memoria en donde fue creado el nuevo objeto. Es decir, que los constructores devuelven la referencia this . Cuando invocamos un método cualquiera sobre un objeto, le pasamos la referencia this al método en forma implícita (es decir que no nos damos cuenta que lo hacemos) por eso es que dentro del método podemos poner this.xxx y sabemos que estamos haciendo referencia a algún atributo o método del objeto actual. En el caso del constructor es similar pero al revés: él nos devuelve la referencia this , y la asigna a la referencia que nosotros hayamos declarado (por ejemplo a "p"): Persona p = new Persona(); tiene el efecto de cargar en "p" la referencia this (o sea, guarda en "p" la dirección de memoria del nuevo objeto recién creado)

• Invocaciones entre Constructores: Un constructor puede invocar (llamar) a otro de su propia clase por medio de :this(...) con o sin parámetros en el encabezado del propio constructor. Es en el único lugar que :this(...) –con paréntesis- puede ser utilizado. Un constructor puede invocar a otro de su clase base por medio de :base(...) con o sin parámetros. Al igual que para el caso anterior, es en el único lugar que podemos utilizar :base(...) –con paréntesis. Por ejemplo, si queremos que un constructor de Asalariado invoque a un constructor de Empleado, escribimos: public Asalariado(...) :base(...) { ...código del constructor de Asalariado... } Se utilizaron puntos suspensivos ya que esta forma de invocar sirve tanto para el constructor por defecto como para los constructores comunes. Si se invocara al constructor por defecto no hay que pasarle ningún parámetro, y si se invocara a algún constructor común se deben pasar los parámetros correspondientes.

• Regla: En estos dos últimos casos, las respectivas invocaciones a :this(...) o a :base(...) deben estar inmediatamente después de los paréntesis del constructor. El motivo es que, conceptualmente, antes de crear una instancia de la subclase, debemos primero crear una instancia de la superclase, para luego extenderla hasta obtener una instancia de la subclase. Así, por ejemplo, si hacemos que un constructor de Asalariado llame a un constructor de Empleado , primero se ejecutara el constructor de Empleado y después el de Asalariado , pues Empleado es la clase base de Asalariado .

• La clase Object : Object es una clase ya existente en C#. La misma es la clase “de más arriba” en la jerarquía de clases. Dicho en complicado: Object es la raíz de la taxonomía de clases de C#. Por lo tanto, no hereda de nadie (pues no tiene a nadie “arriba”) y a su vez todas las clases heredan de ella, ya sea en forma directa o indirecta. Si aplicamos el Principio de Reemplazabilidad a esto, obtenemos que cualquier objeto de cualquier clase será también de tipo Object . Una consecuencia de esto es que si declaramos una referencia de tipo Object , podemos apuntar (referenciar) con ella a cualquier objeto.

Page 21: Obligatorio 2006

Notas sobre Orientación a Objetos Página 21 de 35 Ing. Jorge Corral

• Regla: Cada vez se invoca a un constructor, tarde o temprano también se invocara a algún constructor de la clase base. El motivo de esto es que para crear un objeto de una clase, antes se deben crear todos los objetos de las clases antecesoras (las que están arriba en la jerarquía de clases). Esto da la sensación de que cuando creamos un objeto, en realidad estamos creando muchos objetos. En realidad debe pensarse que el primer objeto que se crea es Object , y luego se va expandiendo ese objeto, agregándole todos los atributos y operaciones definidos en la jerarquía de clases, hasta llegar a tener un objeto completo de la clase a la cual le hicimos new. Se profundizará esto en puntos siguientes y en el punto Encadenamiento de Constructores.

• Regla: En un constructor, si se invoca a :this(...) no se puede invocar a :base(...) y viceversa. El motivo es que cuando invocamos a :this(...) sabemos que en algún momento invocará a algún constructor de la superclase (ver la Regla anterior) y lo mismo pasará con :base(...) . Por lo tanto, si pudiéramos invocar a ambos, estaríamos creando dos objetos de la superclase para el mismo objeto de la subclase!!! Esto sería un error, por tanto no se pueden utilizar ambas invocaciones al mismo tiempo.

• Encadenamiento de Constructores: Los constructores se invocan automáticamente y en cadena a través de la jerarquía de clases definida. Es decir, cuando invocamos a un constructor de Asalariado , por ejemplo, lo primero que hace es invocar al constructor por defecto de Empleado (su superclase) si es que nosotros no invocamos a :base(...) ni a :this(...) . Esto asegura que todos los constructores, tarde o temprano, llamen a los constructores de su superclase antes de ejecutarse ellos mismos. Por ejemplo, el constructor de Empleado lo primero que hace en invocar al constructor de Object (su superclase). El encadenamiento siempre se termina en Object pues es el la clase de mayor jerarquía, la cual no tiene superclase.

• Regla: Como consecuencia de lo anterior, si luego de los paréntesis del constructor no se pone ni :base(...) ni :this(...) o alguna de sus variantes con parámetros, C# inserta la invocación a :base() automáticamente. Sólo se insertarán automáticamente invocaciones al constructor por defecto (sin parámetros) de la superclase, y nunca a un constructor común. Por eso es que los paréntesis aparecen vacíos.

• Comentario sobre el Encadenamiento de Constructores: Debido a lo anterior, como se llaman en cadena y ninguno hace nada hasta llegar a Object , el primer constructor que efectivamente se ejecuta es el de Object , seguido por todos los constructores que lo invocaron, en orden inverso a la invocación. Es decir que si bien los constructores se invocan de “abajo hacia arriba”, en realidad se ejecutan de “arriba hacia abajo”, ejecutándose primero el constructor de Object y por último el que queríamos, por ejemplo el de Asalariado .

Page 22: Obligatorio 2006

Notas sobre Orientación a Objetos Página 22 de 35 Ing. Jorge Corral

• Cuidado: Ya que si en un constructor no invocamos explícitamente a algún constructor de la superclase o a algún constructor de la propia clase, como C# invoca automáticamente a :base() , es decir el constructor por defecto de la superclase, si éste no existe (porque el programador definió sólo constructores comunes) se producirá un error. Este error es particularmente engañoso para quienes comienzan a programar orientado a objetos, pues casi podríamos decir que el error no es nuestra culpa, porque se produce ya que C# invoca automáticamente al constructor por defecto de su superclase cuando este no existe. Es decir, es un error producido por la invocación automática al constructor por defecto de la superclase cuando nosotros no invocamos a ningún constructor.

• Regla: Tanto :base(...) como :this(...) pueden aparecer solamente dentro de constructores y no en ningún otro método. Cuidado: estamos hablando de invocaciones entre constructores, ya sea de la misma clase (:this(...) ) o de la clase base (:base(...) ), por eso siempre utilizamos los paréntesis!!!

•••• Idea: Una idea interesante es tener un constructor que reciba como parámetro un objeto del mismo tipo del que está construyendo, de manera de poder inicializar al nuevo objeto con ciertos valores del que le pasan como parámetro. Por ejemplo: public Persona(Persona p) { ...tomar los valores de "p" y con ellos iniciali zar la instancia actual... }

• Idea: Otra idea interesante es definir un constructor “completo” (el término "completo" es completamente inventado, valga la redundancia) que reciba todos los parámetros necesarios para inicializar un objeto. Luego, los demás constructores de esa clase podrán invocar a éste, pasándole los parámetros que ellos reciban e inicializando los que no reciban. Además, ese constructor “completo” sería el único que invocaría a :base(...) todos los demás lo invocarían a el mediante :this(…) .

• Definición de Sobrecarga de Operaciones: Es la capacidad que tiene un lenguaje de permitir que varias operaciones tengan el mismo nombre, pero todas recibiendo diferentes parámetros. Un ejemplo de sobrecarga seria declarar dos operaciones llamadas sumar, una que reciba dos números enteros y otra que reciba dos números reales: public int sumar(int a, int b) y public double sumar(double a, double b) . Ambas operaciones se llaman igual, pero son operaciones distintas. Un ejemplo muy claro de utilizar sobrecarga, son los constructores, pues todos se llaman igual (y a su vez igual que la clase que los contiene).

• Diferencia entre Redefinición y Sobrecarga: Es importante resaltar la diferencia entre los conceptos de Redefinición y de Sobrecarga, y sobre todo si se ven sus nombres en ingles (overriding y overloading respectivamente). La Redefinición trata de la misma operación, con diferentes métodos. La Sobrecarga trata de diferentes operaciones, con diferentes métodos.

Page 23: Obligatorio 2006

Notas sobre Orientación a Objetos Página 23 de 35 Ing. Jorge Corral

• Más sobre la referencia this : this es en realidad una referencia al "objeto actual". El motivo de necesitar una referencia al objeto actual surge ya que cuando estamos programando la clase, escribiendo su código, no sabemos cuantos objetos se van a crear a partir de ella, ni mucho menos los nombres de las referencias a esos objetos. Entonces, desde dentro del código de la clase: ¿Como hacemos referencia al objeto actual, sea cual fuere este, y se llame como se llame la referencia a este? La respuesta es utilizando la palabra reservada this . Recordar que en ingles this significa "este". Ahora bien: ¿Cuando y por qué necesitamos hacer referencia al objeto actual, sea cual fuere este? Daremos dos ejemplos, los más frecuentemente utilizados, en el siguiente punto.

•••• Un ejemplos del uso de this : El primer caso es cuando un parámetro de un método se llama igual que un atributo de la clase. Por ejemplo, en la clase Persona , que tiene atributos nombre y edad , si tenemos un constructor que reciba un parámetro nombre y otro parámetro edad , entonces los parámetros se llaman igual que los atributos! De más esta decir que esta situación se resuelve cambiando los nombres!!! Entonces, dentro del constructor, colocaremos código como: nombre = nombre; y como edad = edad; Estas dos asignaciones son perfectamente inútiles, y no sirven para nada. El motivo es que cada vez que escribimos la palabra "edad" adentro del constructor, el lenguaje creerá que nos referimos al parámetro del constructor, y no al atributo de la clase. El motivo de esto es por las reglas de alcance de C#. Entonces: ¿Como hacemos para decirle a C# que la palabra "edad" de la izquierda hace referencia al atributo de la clase, mientras que la de la derecha hace referencia al parámetro del constructor? R: escribimos: this.edad = edad; y lo mismo para el nombre.

•••• Otro ejemplo del uso de this : El segundo ejemplo es (mucho) mas útil. Imaginemos que estamos codificando la clase Circulo , y que estamos codificando un método que recibe un objeto Circulo y devuelve el mayor de los dos: el recibido como parámetro o el actual. Esto nos permitiría escribir cosas como: mayor = c1.masGrande(c2); La pregunta es: ¿Qué código debe tener el método masGrande() ? R: el siguiente… public Circulo masGrande(Circulo c) { if (this.radio > c.radio) return this; else return c; }

Page 24: Obligatorio 2006

Notas sobre Orientación a Objetos Página 24 de 35 Ing. Jorge Corral

POLIMORFISMO Y CONCEPTOS RELACIONADOS

• Definición de Polimorfismo: Polimorfismo es la capacidad de asociar diferentes métodos a la misma operación. Esto quiere decir que para lograr un comportamiento polimórfico, llamamos a una operación que sabemos puede derivar en dos o más alternativas, dependiendo del caso (a continuación se verá de qué depende). Cada alternativa es un método.

• Definición de Información Estática: Es la información brindada por el tipo de la referencia. Supongamos una referencia “e” declarada como de tipo Empleado . Sin importar a que clase se le hace un new, la información estática de la referencia dirá que “e” es de tipo Empleado . Por lo tanto, dada la siguiente declaración e instanciación: ClaseX ref = new ... la información estática dirá que la referencia ref es de tipo ClaseX , pues así ha sido declarada, sin importar a que clase se ha instanciado. Es la información que usualmente utiliza el compilador para dar algunos de sus mensajes de error.

• Definición de Información Dinámica: Supongamos, siguiendo el ejemplo anterior, que a la referencia “e” le hacemos un new Asalariado(); Primero que nada, esto es válido pues todos los asalariados son empleados, ya que heredan de Empleado (principio de Reemplazabilidad) La información estática dirá que “e” es de tipo Empleado , pues es lo que dice la declaración de “e”. Pero, evidentemente no está apuntando a un empleado, sino que está apuntando a un asalariado, porque hicimos un new Asalariado() . Entonces, la información dinámica dirá que “e” es de tipo Asalariado , pues la información dinámica se fija a qué está apuntando realmente, y no le importa el tipo de “e”.

• Comparación entre Información Estática y Dinámica: Otra forma de ver estos conceptos es la siguiente. La información estática se llama estática pues una vez que se declaró una referencia como de cierto tipo, esto no se puede cambiar. Si ya codificamos Empleado e; entonces en el mismo código no podemos darle otro tipo a la referencia “e”. Por eso es una información que no cambia (o sea, estática). Sin embargo, la misma referencia “e” la podemos hacer apuntar a diferentes tipos de objetos (siempre y cuando sean instancias directas o indirectas de Empleado , según lo que exige el Principio de Reemplazabilidad). Dado que la referencia apuntará a diferentes tipos de objetos, la información dinámica se llama dinámica porque puede cambiar de línea en línea dentro del código.

• Coincidencia entre la Información Estática y Dinámica: La información estática y dinámica no tienen porque coincidir, pero pueden hacerlo. Por ejemplo, si hacemos: Empleado e = new Empleado(); en este caso ambas coinciden.

• Definición de Despacho: Despachar un método quiere decir ejecutar el método asociado a la operación invocada sobre la referencia.

Page 25: Obligatorio 2006

Notas sobre Orientación a Objetos Página 25 de 35 Ing. Jorge Corral

• Despacho Estático y Dinámico: Debido a que existe la herencia y el Principio de Reemplazabilidad, no queda claro qué hay que hacer cuando se pide algo como: e.calcularSueldo(); si esa operación existe tanto en Empleado como en Asalariado y Jornalero . Decimos que el despacho es estático cuando el compilador despacha el método basándose en la información estática, y que el despacho es dinámico cuando se despacha el método basándose en la información dinámica. Por tanto, si la operación calcularSueldo() existe tanto en Empleado como en Asalariado , y tenemos la sentencia: Empleado e = new Asalariado(); e invocamos a calcularSueldo() sobre la referencia "e" (e.calcularSueldo() ) el método despachado puede ser tanto el de la clase Empleado como el de la clase Asalariado . Esto dependerá de que despacho utilice el lenguaje. Si utiliza despacho estático, estaremos despachando el método que aparece en la clase Empleado (pues "e" esta declarada como de tipo Empleado ) mientras que si utiliza despacho dinámico, estaremos despachando el método que aparece en la clase Asalariado (pues "e" apunta realmente a un objeto de tipo Asalariado ).

• Base del Polimorfismo: La base del polimorfismo es utilizar despacho dinámico. De esta forma, si tenemos la operación calcularSueldo() en las tres clases mencionadas, y si “e” está declarada como de tipo Empleado , el despacho dinámico asegura que se invocará al método adecuado dependiendo de a qué está apuntando realmente la referencia “e” (es decir, basándose en la Información Dinámica). Por ejemplo, si “e” apunta a un Asalariado (aunque “e” se haya declarado de tipo Empleado ) al llamar a e.calcularSueldo() se despachará el método calcularSueldo() de la clase Asalariado .

• Base del Polimorfismo 2: Entonces, la gran pregunta es: ¿Cómo lograr despacho dinámico para permitir un comportamiento polimórfico? La respuesta viene por tres lados: utilizar operaciones virtuales, utilizar operaciones abstractas y utilizar interfaces. Esas son las tres formas que tenemos de obtener un comportamiento polimórfico para una operación en C#.

• Polimorfismo a través de Operaciones Virtuales: Este caso se da si calcularSueldo() fuese un método virtual (virtual ) en la clase Empleado , y que luego es redefinido en las sub-clases (override ). Necesitamos herencia para obtener este tipo de polimorfismo.

• Polimorfismo a través de Operaciones Abstractas: Este caso se da si calcularSueldo() fuese una operación abstracta (sin método) definida en Empleado y redefinida en las sub-clases. también necesitamos herencia para obtener este tipo de polimorfismo.

Page 26: Obligatorio 2006

Notas sobre Orientación a Objetos Página 26 de 35 Ing. Jorge Corral

• Polimorfismo a través de Interfaces (se utilizarán conceptos aún no explicados): Este caso se da si IEmpledo fuese una Interfaz que tanto la clase Asalariado como la clase Jornalero implementan. Así, podemos tener referencias “e” de tipo IEmpleado (que en realidad apuntan a objetos de tipo asalariado o jornalero) y como ambos deben implementar calcularSueldo() por ser una operación de la interfaz, ambos tendrán un método distinto para la misma operación. Esta forma de obtener comportamiento polimórfico no necesita de la herencia, y justamente por eso es la más poderosa forma del polimorfismo, porque no nos ata a que tengamos que heredar de una clase si queremos un comportamiento polimórfico. Teniendo en cuenta que no se puede heredar de más de una clase (no hay herencia múltiple ni en C# ni en Java) el hecho de obtener un comportamiento polimórfico sin “pagar” el precio de la herencia no es poca cosa. Recordar también que no hay límites para la cantidad de interfaces que una clase puede implementar, por lo que este tipo de polimorfismo no impone ninguna restricción sobre las clases Asalariado y Jornalero (en los otros dos casos de polimorfismo, se impone la restricción de que las clases tienen que esta relacionadas por medio de la herencia) más que implementar las operaciones de la interfaz.

• Beneficios del Polimorfismo: Una de las situaciones en donde se ve más claramente los beneficios del comportamiento polimórfico, es cuando necesitamos recorrer una colección de objetos de diferente tipo y ejecutar cierta acción sobre cada uno de ellos. Por ejemplo, si tenemos una colección de objetos que pueden ser de tipo Asalariado , Jornalero o Vendedor (todos subclases de Empleado ), y no sabemos en cada lugar de la colección si habrá un objeto de un tipo u otro, y queremos sumar todos los sueldos de todos los empleados, sin importar su tipo, entonces podremos recorrer toda la colección, y para cada lugar pedir calcularSueldo() , sin saber a que tipo de objeto le estamos pidiendo el sueldo. Sabemos que esta invocación devolverá el sueldo del empleado al que realmente apunte la referencia, y que esto se hará automáticamente (gracias al despacho dinámico). Si no utilizamos el polimorfismo, tendríamos que averiguar que tipo de objeto es el actual, y pedirle la operación correspondiente a su tipo, pues cada tipo tendría una operación distinta.

• Casteo: El up-cast es un casteo de un tipo hacia otro tipo superior en la jerarquía de clases. Como no hay herencia múltiple, todo tipo tiene solamente un super-tipo (clase base), por lo que el up-cast siempre se puede hacer sin problemas, y no es necesario hacerlo explícitamente. Esto se dice que es un casteo implícito, pues el programador no tiene que hacer nada. Pero, si el casteo es hacia abajo (down-cast, de un tipo hacia otro tipo inferior en la jerarquía de clases) como la herencia permite varias clases derivadas de una misma clase base, ahí sí tenemos que decirle para cual clase queremos castear. Por ejemplo, si tenemos una referencia “e” que apunta a un Asalariado pero fue declarada como de tipo Empleado , y queremos verla como de tipo Asalariado , tenemos que hacer un down-cast explícito poniendo (Asalariado)a lo cual devolverá un objeto de tipo Asalariado .

• Comentario sobre Casteo: El casteo es una herramienta necesaria pues, como se dijo en puntos anteriores, podemos tener una referencia de un tipo apuntando a un objeto de otro tipo diferente (cuando la información estática y dinámica difieren). Entonces, vamos a querer "ver" al objeto como de un tipo o como de otro, depende de lo nos convenga. Para poder verlo como de un tipo u otro, es que usamos el casteo.

Page 27: Obligatorio 2006

Notas sobre Orientación a Objetos Página 27 de 35 Ing. Jorge Corral

• Cuidado: Castear no quiere decir modificar el objeto. Solamente permite “ver” el objeto como de un tipo o como de otro tipo, para que podamos invocarle operaciones y pedirle atributos. Podemos pensar que un objeto tiene diferentes "caras", cada cara brindando los atributos y métodos de todas las clases superiores en la jerarquía de clases, mas una "cara" mostrando los atributos y métodos de la propia clase. Entonces, el casteo permitiría “girar” el objeto, permitiéndolo ver mediante una de esas "caras".

• Ejemplo de up-cast: Supongamos que tenemos una referencia "a" de tipo Asalariado , y queremos verla como de tipo Empleado , cosa que sabemos se puede por Reemplazabilidad. Entonces, simplemente definimos una nueva referencia "e" de tipo Empleado , SIN HACER NEW, y escribimos: e = a; Como se dijo antes, esto es un casteo implícito, pues no nos damos cuenta del casteo. Podríamos darnos cuenta si escribimos: e = (Empleado)a; cosa que es perfectamente válida, pero innecesaria. No es necesario decirle al compilador que castee hacia Empleado , pues el up-cast solo tiene una opción posible, pues cada clase solo tiene una clase base. Entonces, el up-cast se trata de ir desde la subclase hacia la clase base, "subiendo" en la jerarquía, por eso lo de up-cast.

• Cuidado 2: Cuando casteamos un objeto hacia un tipo, debemos tener cuidado de pedirle las cosas que estén definidas dentro de la clase a la cual casteamos (o cosas heredadas). Es decir, si tenemos un objeto de tipo Asalariado pero lo estamos viendo como de tipo Empleado (porque se hizo un up-cast hacia Empleado ) entonces no podemos pedirle atributos u operaciones de Asalariado , aún cuando las tenga. Se supone que esto no es problema, porque por algo queríamos verlo como un Empleado , no?

• Ejemplo de down-cast: Supongamos que tenemos una referencia "e" de tipo Empleado , pero sabemos (luego veremos como supimos) que en realidad apunta a un Asalariado . Entonces, para ver al objeto como lo que es (Asalariado ), simplemente lo casteamos hacia abajo. Para eso, definimos una referencia "a" de tipo Asalariado y escribimos: a = (Asalariado)e; Notar que este casteo hacia abajo es explícito: nosotros tenemos que poner entre paréntesis hacia donde vamos a bajar, porque la referencia "e" perfectamente podría apuntar haber apuntado a un Jornalero ! Entonces, si no le decimos hacia donde bajar, y tenemos dos opciones (bajar hacia Asalariado o hacia Jornalero ) el compilador no sabrá que hacer. Notar que hay que saber de antemano que la referencia "e" apuntaba a un Asalariado . Si no llega a ser así, el casteo dará un error, ya que un Jornalero no puede "verse" (castearse) como un Asalariado ni viceversa. Entonces, leer atentamente el siguiente punto para aprender como saber que efectivamente apunta a un Asalariado o a un Jornalero .

Page 28: Obligatorio 2006

Notas sobre Orientación a Objetos Página 28 de 35 Ing. Jorge Corral

• Como saber a que apunta una referencia: Sabemos por Reemplazabilidad que una referencia puede apuntar a objetos de su propio tipo o de un subtipo. Entonces, para saber a que tipo de objeto apunta realmente una referencia en C# utilizamos la palabra reservada is . La palabra reservada is es en realidad un operador que recibe dos parámetros, uno a cada lado, y devuelve un bool . La sintaxis es: (e is Asalariado) esto devolverá true en caso de que "e" efectivamente este apuntando a un Asalariado , y false en otro caso (que este apuntando a un Jornalero ) Por tanto, la forma clásica de utilizarlo es como condición de un if : if (e is Asalariado) {...} else {...} Esto debe hacerse cuando queremos efectuar un down-cast y no estamos seguros de que poner entre los paréntesis del casteo, ya que si le erramos, el error saltará no al compilar, sino cuando el programa este corriendo, cosa que no es para nada deseable.

• Cuidado 3: Cuando hacemos casteos, para ver un objeto como de un tipo u otro, nunca hay que hacer new. Siempre hay que declarar (no instanciar) una nueva referencia de algún tipo, pero no hacerle new. El motivo es que no queremos un nuevo objeto, queremos ver ese objeto como de otro tipo. Por eso el new no se debe usar cuando casteamos. Notar que, una vez más, utilizar un lenguaje preciso es valioso, pues acabamos de decir que hay que declarar una nueva referencia, pero no instanciarla. Declarar una referencia es, por ejemplo, Empleado e; mientras que instanciarla es, por ejemplo: e = new Empleado();

Page 29: Obligatorio 2006

Notas sobre Orientación a Objetos Página 29 de 35 Ing. Jorge Corral

INTERFACES

• Definición de Interfaz: Una interfaz es un conjunto de operaciones al cual se le da un nombre. Por ejemplo, la interfaz IPepe consta de dos operaciones: void oper1() y bool oper2(int i) . La primera operación no recibe nada ni devuelve nada, y la segunda recibe un entero y devuelve un booleano.

• Convención: Los nombres de las interfaces suelen comenzar con la letra “i” mayúsculas y seguir con la primera letra (también en mayúsculas) del nombre que se le quiere dar: INombre .

• Comentario: Ya que una interfaz es una colección de operaciones, la interfaz no tiene código "útil". Recordar que una cosa es una operación, y otra cosa es un método. Las interfaces no tienen método, por definición. Como consecuencia de esto, se puede decir que una interfaz "no hace nada", ya que no tiene código "útil" que haga algo. Cuidado: esto no quiere decir que no sirvan para nada, solo que no pueden ejecutar código, como una clase si puede, por ejemplo. Las interfaces tampoco tienen atributos.

• Regla: Ya que una interfaz no tiene ni atributos ni métodos, a partir de una interfaz no podemos crear un objeto, porque el mismo no tendría estado (recordar que el estado de un objeto está dado por el valor de los atributos) ni comportamiento (recordar que el comportamiento de un objeto está dado por los métodos). Entonces, una interfaz efectivamente no sirve para crear objetos. Como consecuencia directa de esto, no le podemos pedir un new a una interfaz. Es decir, que algo del tipo i = new IPepe(); es incorrecto.

• Cuidado: Si bien no se le puede hacer un new a una interfaz, si se puede declarar una referencia como de tipo interfaz. Es decir, que algo del tipo IPepe r; es correcto, y lo que hace es definir una referencia de tipo IPepe . Luego se verá porque interesa hacer esto. Como consecuencia de este punto, podemos decir que las interfaces, al igual que las clases, definen un nuevo tipo, por lo que una referencia puede ser de tipo interfaz y apuntar a un objeto (aunque ese objeto sabemos que no será una instancia de la interfaz, porque no se puede instanciar una interfaz!!!)

• Motivo: La idea de tener interfaces es para obligar a las clases a que tengan ciertas operaciones. Una interfaz por si sola, no sirve para nada si no hay una (o varias) clase que se relacione con ella. La relación entre una clase y una interfaz se llama implementación o realización. Se dice que la clase implementa o realiza la interfaz, por lo tanto es la clase la que tiene que tener algún código que indique esta relación, no la interfaz. Cuando una clase implementa una interfaz, tiene la obligación de darle un método a todas y cada una de las operaciones definidas en la interfaz. Entonces, si la interfaz contiene una operación double calcRaiz(double num) todas las clases que implementen la interfaz deben tener un método que justamente implemente esa operación, es decir que las clases que implementan la interfaz deben saber como calcular la raíz cuadrada de un número, en este caso. Por eso se dice que cuando una clase implementa una interfaz, se esta "comprometiendo" a cumplir con las operaciones que la interfaz define (o sea a ponerle un método a cada una de ellas).

Page 30: Obligatorio 2006

Notas sobre Orientación a Objetos Página 30 de 35 Ing. Jorge Corral

• Sintaxis: Si queremos que la clase ClaseA implemente la interfaz IPepe , la sintaxis es exactamente la misma que para la herencia entre clases: se utilizan los dos puntos, seguidos del nombre de la interfaz que queremos implementar. Ejemplo: public class ClaseA : IPepe . Si ocurre que la clase ClaseA además de implementar la interfaz IPepe , hereda de otra clase, se pone primero la clase base y luego la interfaz, separados por coma: public class A : ClaseBase, IPepe

• Regla: Una clase puede implementar cualquier cantidad de interfaces. Esto no pasa con la herencia, donde una clase solo puede heredar de una. Siempre se colocaran los dos puntos, seguidos de la clase base (si existe) primero y luego, sin importar el orden, todos los nombres de las interfaces implementadas separados por comas. El sentido por el cual una clase puede implementar muchas interfaces es para obligar a la clase a que presente (tenga método para) todas las operaciones de todas las interfaces implementadas. Claramente, cuantas más interfaces implementa una clase, mas "obligaciones" tiene.

• Motivo II: En realidad, el hecho de obligar a una clase a que presente ciertas operaciones con su método, no es el objetivo último del uso de interfaces. Lo interesante que tienen ellas, es que permiten a todas las demás clases pedirle con total tranquilidad cualquiera de las operaciones de la interfaz define, pues se supone que si implementa la interfaz, tendrá un método para todas sus operaciones. Así, si la clase ClaseA implementa la interfaz IPepe con la operación void oper1() entonces cualquier otra clase, supongamos ClaseC , puede pedirle a objetos de ClaseA que ejecuten el método oper1() , ya que ClaseA se "comprometió" a darle método a las operaciones de IPepe .

• Comentario: Del punto anterior se entiende porqué a las interfaces se las suele comparar con un "contrato", ya que la interfaz dice QUE es lo que se debe hacer, pero no COMO, mientras que la clase que la implementa es la "proveedora" de esos servicios, y la clase que utiliza los servicios (las demás clases que invocan las operaciones) son los "clientes" de ese servicio. En resumen, la interfaz es el contrato que especifica un servicio (dado por las operaciones de la interfaz). El proveedor del servicio es la clase que implementa la interfaz, y los clientes son las clases que invocan (utilizan) las operaciones.

Page 31: Obligatorio 2006

Notas sobre Orientación a Objetos Página 31 de 35 Ing. Jorge Corral

• Motivo III (AVANZADO): Otro motivo, tal vez un poco más oculto que el anterior, para utilizar interfaces, es que permiten que el proveedor y el cliente "no se conozcan". Particularmente, permite al cliente NO conocer quien le esta proveyendo del servicio, es decir, del que le está ejecutando los métodos invocados por el cliente. Por esto es que se suele decir que las interfaces sirven para separar a las clases clientes de las clases proveedoras. Imaginemos la siguiente situación: tenemos dos clases clientes C1 y C2 que les interesa invocar las operaciones de la interfaz IPepe , y que tenemos dos clases proveedoras P1 y P2 que ambas implementan la interfaz IPepe , seguramente con métodos distintos para las mismas operaciones. Por ejemplo, P1 utiliza una forma antigua de calcular la raíz cuadrada de un número, mientras que P2 utiliza una nueva forma de cálculo. Recordar que en definitiva, ambos proveedores proveen exactamente lo mismo: permiten calcular la raíz cuadrada de un número. Suponemos, claro esta, que a los clientes no les va a importar de qué forma se calcula la raíz cuadrada, siempre que el resultado sea correcto. Entonces, desde adentro de C1 podemos tener una referencia "i" de tipo IPepe a la cual le pedimos i.calcRaiz(9) y nos devolverá 3, pero como puede verse, no sabe ni le interesa cual de las dos clases proveedoras esta haciendo el calculo. Es mas, si luego de haber calculado un par de raíces, cambiamos de proveedor, a los clientes no les importara. Por esto es que se dice que las interfaces sirven para mantener alejados a los clientes de los proveedores. Si no tuviéramos interfaces, tendríamos que haber declarado "i" ya sea como de tipo P1 o como de tipo P2, con lo cual los clientes sabrían exactamente que proveedor les están dando el calculo. También se dice que las interfaces sirven para romper las dependencias entre las clases (entre las proveedoras y las clientes).

• Polimorfismo a través de Interfaces: Como se dijo en puntos anteriores, las interfaces son una muy buena forma de obtener comportamiento polimórfico, algo muy deseable en la orientación a objetos. La principal diferencia de este tipo de polimorfismo frente a los otros dos (por operaciones abstractas y por métodos virtuales) es que no requiere del uso de la herencia, por lo tanto podemos usar el polimorfismo sin tener que heredar de una clase en común. Esto siempre es bueno, pues no estamos restringiendo el diseño de la solución, ya que si heredamos de una clase para poder usar polimorfismo, entonces no podemos heredar de otra. Pero el otro motivo por el cual resulta útil usar las interfaces para polimorfismo, es que podemos lograr polimorfismo entre clases completamente diferentes. Cuando utilizamos la herencia, todas las subclases tienen algo en común: la clase base. Pero ¿Qué ocurre si tenemos clases tan diferentes que resulta imposible definir una clase base para ambas? Entonces, utilizando una interfaz podemos hacer que ambas implementen la interfaz y lograr polimorfismo, sin tener que forzar a utilizar la herencia. Recordar que la herencia implica tomar cosas en común de varias clases y factorizarlas en una nueva clase que será la clase base de ellas. La clase base se supone que tiene atributos y métodos comunes a todas sus subclases, por lo que las subclases están fuertemente relacionadas entre si (son todas “hermanas”). En todos los casos en que no encontremos fuertes relaciones entre las clases, pero sin embargo deseamos utilizar polimorfismo, las interfaces son la mejor solución.

• Comentario: Se suele decir que la herencia es una forma muy "fuerte" de relacionar clases, mientras que la implementación de interfaces es mas "liviana" ya que las clases pueden tener menos que ver entre sí, como se vio en el punto anterior.

Page 32: Obligatorio 2006

Notas sobre Orientación a Objetos Página 32 de 35 Ing. Jorge Corral

ASOCIACIONES ENTRE CLASES

• Definición de Asociación: Una asociación entre dos clases es una relación duradera entre ambas clases. Por ejemplo, si en la realidad ocurre que todos los clientes tienen una cuenta corriente, entonces la clases Cliente y CuentaCorriente tendrán una asociación entre si. Las asociaciones tiene dirección, y pueden ser unidireccionales en un sentido, en el otro o bidireccionales.

• Implementar Asociaciones: Este punto explicara como implementar una asociación, es decir, como codificarla en un lenguaje de programación. Primero que nada, se debe saber que las asociaciones entre clases no se corresponden con una palabra reservada. Por ejemplo, una clase se corresponde con la palabra reservada class , lo mismo que interfaz con interface , o herencia con los dos puntos en C#. En todos estos casos hay una relación directa entre el concepto y su implementación. Este no es el caso con las asociaciones, es decir, que no hay una palabra reservada del estilo association , o sea que debemos implementarlas de otra forma. Para ver de qué forma, veremos primero cual es el objetivo de asociar clases.

• Objetivo: La idea de asociar clases es que luego, las instancias de esas clases, también estarán relacionadas entre sí. Es decir, si asociamos la clase Cliente con la clase CuentaCorriente , entonces las instancias de Cliente estarán relacionadas con una instancia de CuentaCorriente . Por lo tanto, las asociaciones entre clases permiten aumentar el tamaño (y complejidad) de un sistema orientado a objetos, para poder así desarrollar sistemas de gran tamaño a partir de bloques constructores (las clases) y vínculos entre ellos (las asociaciones).

• Definición de Link: Cuando hablamos a nivel de clases, decimos que las clases están "asociadas", pero cuando hablamos a nivel de objetos (por supuesto instancias de esas clases asociadas) decimos que los objetos están "linkeados". Muchas veces se verá utilizar intercambiadamente estos conceptos, y no será un problema siempre y cuando se sepa la diferencia y se sepa el contexto. Se dice entonces que un link es una instancia de una asociación, y que además no puede existir un link entre dos objetos si sus respectivas clases no están asociadas.

• Implementar Asociaciones, Parte II: Ahora que sabemos el motivo por el cual queremos asociar clases (para poder linkear sus instancias) es que podemos pensar en cómo codificar las asociaciones de clases. ¿Cómo hacemos para que, en tiempo de ejecución, un objeto este linkeado con otro? Porque esto es lo que queremos! Podemos utilizar una idea fundamental de la orientación a objetos y que ya la venimos manejando desde el principio: la referencia. Una referencia nos permite "apuntar" a un objeto, entonces parece ser la herramienta adecuada para que dos objetos se apunten entre sí! Y efectivamente lo es. Entonces, si queremos que todos los clientes tengan una cuenta corriente, en la clase Cliente además de todos sus atributos actuales, le agregamos otro que será una referencia de tipo CuentaCorriente . Por tanto, agregamos algo del estilo: CuentaCorriente c; dentro de la clase Cliente . De esta manera, los objetos Cliente podrán apuntar a objetos CuentaCorriente .

• Definición de pseudoatributo: A estos atributos que agregamos para poder asociar dos clases, se les suele llamar pseudoatributos, dejando el término atributo para aquellos datos del problema (de la “vida real”) que el sistema debe guardar.

Page 33: Obligatorio 2006

Notas sobre Orientación a Objetos Página 33 de 35 Ing. Jorge Corral

• Definición de Navegabilidad: La navegabilidad indica el sentido de la asociación entre clases, y por consiguiente, el sentido también de los links entre sus objetos. Una asociación (y también sus links) puede ser de tres tipos: bidireccional, de una clase a la otra, o de la otra a la una. Si una asociación es bidireccional, entonces podremos navegarla desde cualquiera de las dos clases hacia la otra. Por ejemplo, dado un objeto Cliente podemos saber su CuentaCorriente , y dada una CuentaCorriente podemos saber de que Cliente es. Si es unidireccional de Cliente a CuentaCorriente , entonces dado un Cliente podemos saber cual es su CuentaCorriente , pero dada una CuentaCorriente no podemos saber de quien es. Análogamente si es unidireccional de CuentaCorriente a Cliente . En principio, sin saber más sobre la realidad, no podemos decir que una solución es mejor o peor que las demás. Son simplemente tres soluciones distintas. La "mejor" dependerá de las funcionalidades del programa a desarrollar.

• Implementar Navegabilidad: La navegabilidad se implementa colocando más o menos referencias entre las clases asociadas. Si queremos una navegabilidad hacia ambos lados (bidireccional), tendremos que colocar una referencia en cada clase que apunte a la otra. Si queremos navegabilidad en un solo sentido (unidireccional), debemos colocar una referencia en la clase de la cual queremos partir, cuyo tipo sea la clase a la cual queremos llegar.

• Definición de Cardinalidad: La cardinalidad indica con cuantas instancias se podrán asociar las instancias de una clase. En el ejemplo del Cliente y CuentaCorriente , la cardinalidad de Cliente a CuentaCorriente es de 1, pues cada instancia de Cliente estará asociada con una sola instancia de CuentaCorriente . Pero también, la cardinalidad entre CuentaCorriente y Cliente también es 1, pues cada CuentaCorriente pertenece a un solo Cliente . Cada asociación presenta dos cardinalidades, una para cada extremo de la asociación.

• Tipos de Cardinalidades: Usualmente hay tres tipos de cardinalidades: 1 a 1, 1 a muchos y muchos a muchos. La primera, 1 a 1, implica que ambas partes solo conocerán a una de la otra parte. El segundo caso, 1 a muchos, implica que una instancia de una de las clases estará asociada con muchas instancias de la otra clase, como podría ocurrir si suponemos que las cuentas corrientes pueden tener varios dueños. El tercer caso, muchos a muchos, implica que no hay restricciones en cuanto a la cantidad de objetos con los que se asocia otro objeto, como podría ocurrir si suponemos que un cliente puede tener varias cuentas, y que cada cuenta puede ser de varios clientes. De todos modos, se puede colocar cualquier cardinalidad, como por ejemplo que cada instancia de una clase esté asociada con 3 a 5 de la otra.

• Importante: Vale la pena aclarar que, generalmente, las cardinalidades no se deben "inventar", ni ser una ocurrencia o capricho del programador. Estas deben ser tomadas de la realidad. O la realidad dice que un cliente tiene una sola cuenta, o dice que puede tener varias cuentas. Dependiendo de la realidad es que colocamos las cardinalidades en una asociación.

Page 34: Obligatorio 2006

Notas sobre Orientación a Objetos Página 34 de 35 Ing. Jorge Corral

• Observación: Para todos los posibles casos de cardinalidades, puede ocurrir que un objeto no esté asociado con ninguno (es decir, que este asociado a cero de los otros). Por ejemplo, puede que tengamos el cliente en el sistema, pero que aun no hayamos creado su cuenta corriente. Aquí, de nuevo, hay que tener cuidado e interpretar la realidad. Tal vez no es correcto que puedan haber clientes sin cuenta corriente, en cuyo caso siempre que creamos un cliente, inmediatamente le creamos su cuenta corriente, así nunca tendrá cero asociaciones. A través de código se puede impedir que un objeto quede sin referencias a otros, si es que se desea controlar esto.

• Implementar Cardinalidades: Dado que las cantidades de objetos con los que se asocia otro objeto puede variar, entonces: ¿Como codificar esto? Ya sabemos como se codifica cuando la cardinalidad es 1: colocando un atributo que sea una referencia cuyo tipo sea la otra clase. Entonces, en realidad solo queda saber como implementar una cardinalidad de "muchos". Para esto, colocamos un atributo que sea una colección de referencias, como puede ser un array, un ArrayList de C# o alguna colección provista por el lenguaje de programación. De esta manera podemos tener muchas referencias a objetos de la otra clase. Si la cardinalidad fuese de "muchos a muchos" sobre una asociación con doble navegabilidad (bidireccional), entonces ambas clases pueden tener un ArrayList en el que guardaran las referencias a los objetos de la otra clase con los cuales están linkeados. En general, es mejor solución utilizar un ArrrayList , ya que su tamaño varía según la cantidad de elementos que tenga, mientras que un array es de tamaño fijo. Recordar que el ArrayList guarda en realidad referencias de tipo Object , y no sabe a lo que apunta. Esto permite (por Reemplazabilidad) guardar referencias de cualquier tipo, porque cualquier tipo es Object , pero hay que recordar de castear las referencias cuando accedemos a los elementos del ArrayList , de lo contrario lo que recibiremos es un objeto de tipo Object al cual no le podremos pedir nada, pues no tendrá los atributos y métodos que deseamos invocar (es decir, que estaremos viendo la "cara" del objeto que es de tipo Object )

• Comentario: Cabe destacar que por el hecho de tener una referencia a otro objeto, no estamos guardando una copia de ese otro objeto. Lo que tenemos en realidad es un alias de ese objeto, un puntero que lo apunta, pero no estamos copiando ninguna información del objeto. Tampoco es lo mismo que guardar la clave del objeto. Por ejemplo, no es lo mismo guardar una referencia a un objeto de tipo Persona que guardar la cédula de esa persona. Si guardamos la cédula, por ejemplo de tipo int , entonces si la persona llega a cambiar su cédula (imagínelo, por el bien del ejemplo!) tenemos que ir a todos los otros que tienen guardada esa clave y cambiarla. Por otro lado, si guardamos una referencia a la persona, podemos cambiar su cédula sin ninguna repercusión. Recordar que la referencia guarda la dirección de memoria del objeto. Esta es una diferencia importante entre el paradigma de orientación a objetos y el paradigma de entidad relación de las bases de datos: en una tabla (concepto que puede ser comparado con una clase) siempre debe haber una clave primaria que permita identificar en forma única a los registros (concepto que puede ser comparado con un objeto) de esa tabla. Esto no ocurre con las clases, las cuales no tiene porque tener ningún atributo que permita diferenciar las instancias (cada instancia es diferente a las demás gracias a la identidad que todo objeto posee). Además, para que un registro “apunte” a otro registro, el primero debe tener una copia de la clave primaria del segundo (llamada clave foránea). Esto no ocurre con los objetos, los cuales en lugar de tener una copia de un atributo del otro, tienen una referencia al otro objeto, que no es lo mismo.

Page 35: Obligatorio 2006

Notas sobre Orientación a Objetos Página 35 de 35 Ing. Jorge Corral

• Repaso sobre el Estado de un Objeto: Antes se dijo que el estado de un objeto era el conjunto de los valores de sus atributos en un instante del tiempo. Esto es correcto siempre y cuando se incluyan a los links como atributos. Si los links no son considerados como atributos, entonces lo correcto es decir que el estado de un objeto es el conjunto de los valores de sus atributos más el conjunto de links. Es decir, es todo lo que “el sabe” (atributos) més lo que “el conoce” (links). Por lo tanto, el estado general de un sistema orientado a objetos, compuesto de objetos y links, se puede modificar de cinco maneras: (i) creando un objeto, (ii) eliminando un objeto, (iii) creando un link, (iv) eliminando un link y (v) modificando el valor de un atributo.