[MÚSICA] [MÚSICA] Hola y bienvenidos a un nuevo video. Vamos a seguir viendo conceptos relacionados con las relaciones de herencia, en particular clases abstractas y el concepto de polimorfismo. Al final del video, podrás leer diagramas de clases que contengan relaciones de herencia, usar clases abstractas y entender las implicaciones del polimorfismo al momento de implementar relaciones de herencia entre clases. Hablemos primero de clases abstractas. El propósito principal de una clase abstracta es ayudar a organizar una jerarquía de conceptos. Por ejemplo, los animales se pueden clasificar en vertebrados o invertebrados, o podríamos clasificar vehículos en automóviles, aviones y barcos, o figuras en elipses, rectángulos y triángulos. En cada ejemplo podemos tener una instancia de las subclases pero no de la superclase, así decimos que la superclase es abstracta, you que no podemos crear objetos de esta misma. Las superclases tienen entonces un nivel de abstracción mayor. Allí podemos definir atributos y métodos que serán heredados por todas las subclases. También podemos declarar métodos, pero debemos indicar que quienes deben definir su implementación son las subclases. Estos son los métodos que llamamos métodos abstractos, que explicaremos con más detalle en un momento. Cuando implementamos en UML una clase abstracta, su nombre se escribe en itálica, respetando las demás convenciones de nombramiento que hemos visto hasta ahora. Tomemos ahora el ejemplo de la superclase Figura, de la cual tres otras clases la especializan, elipse, rectángulo y triángulo. Notemos que figura está en itálica y que usamos la notación de la flecha de triángulo vacío para indicar cuáles son sus subclases. Ahora, vamos a definir los atributos con base en nuestra clase abstracta, alto y ancho. También unos métodos de base con una implementación sencilla, darAncho y darAlto, que retornan los atributos del mismo nombre. Todas las subclases que hereden de Figura no tendrán que implementar estos métodos, you que existen. Como figura es una clase abstracta, algunos de sus métodos no tendrán implementación y es probable que necesiten usar los atributos que definimos. ¿Cómo podemos entonces declarar la visibilidad de los atributos sin comprometer el encapsulamiento? Recuerda que si los declaramos como públicos, todos podrán accederlos; y si son privados, sus subclases no tendrían acceso a ellos. En UML existe un tercer tipo de visibilidad, protegida. Los atributos protegidos son visibles para las clases que especializan a una superclase y nada más. Con eso hemos resuelto nuestro problema. Ahora, como podemos ver, los atributos protegidos no se indican con un símbolo de menos o con un símbolo de más, sino con un numeral. Hablemos ahora de métodos abstractos. Como mencionamos anteriormente, las clases abstractas pueden determinar que sus subclases deben declarar e implementar ciertos métodos de los que ellas solo proveen el nombre, el tipo y los parámetros. Esto es lo que llamamos un método abstracto, un método que todas las subclases deben implementar. En nuestro ejemplo, supongamos dos métodos abstractos, darÁrea y darPerímetro, ambos métodos van a variar de acuerdo con la figura. No es igual calcular ambas medidas en un elipse que en un rectángulo o en un triángulo. Luego, tiene sentido que cada subclase provea su propia implementación. En UML, los métodos abstractos se representan con su nombre en itálica, su visibilidad será pública o protegida, el tipo de retorno debe ser consistente, por supuesto. Ahora, existe un caso algo particular de las clases abstractas del que debemos hablar, las interfaces. Las interfaces son clases que no proveen implementación de ninguno de sus métodos. Esto es, todos sus métodos son abstractos. Adicionalmente, sus atributos no son protegidos, sino públicos. Y mientras las clases abstractas nos permiten especializar y clasificar los elementos de nuestro modelo, las interfaces están más orientadas a proveer un contrato funcional con el que todas las clases que la implementan, sí, que la implementan, no que la especializan, deben comprometerse. Revisemos entonces por un momento ahora la notación de conjuntos para representar clases abstractas. De nuevo entonces tengamos ahora cinco objetos. Este va a ser una instancia de la clase triángulo, dos de la clase rectángulo y dos de la clase elipse. ¿Qué es lo que sucede? Como vamos a manejar clases abstractas, no podemos crear instancias de la clase figura porque no están completas. Sin embargo, siguen definiendo un conjunto de la superclase donde van a estar todas las subclases. Entonces, la diferencia entre los subconjuntos y el conjunto de la figura tiene que estar vacía, no puede haber espacio para crear instancias de la clase figura. En otras palabras, este espacio tiene que estar dividido exactamente en tres porciones para representar que solo de la clase figura, solo pueden existir instancias de la clase triángulo, de la clase rectángulo o de la clase elipse. Esta es la diferencia de cuando representamos clases abstractas utilizando el mecanismo de herencia. En este caso, a diferencia de la herencia tradicional, no podemos crear instancias de la superclase y solo instancias de las subclases. Examinemos ahora el polimorfismo, una de las consecuencias más interesantes de las relaciones de herencia cuando vamos a programar en un lenguaje orientado a objetos. Para ilustrarlo mejor, pensemos en nuestro ejemplo y agreguemos dos clases adicionales, una clase círculo, que hereda de elipse, y una cuadrado, que hereda de rectángulo. Sobreescribimos los métodos darÁrea y darPerímetro en las subclases círculo y cuadrado para implementarlos ajustadamente. Primero que todo, el polimorfismo es la propiedad que tienen todos los objetos en una relación de herencia de poderse instanciar como objetos de su misma clase o de una de sus superclases, esto es una instancia de elipse, por ejemplo, es también una instancia de figura. Una instancia de la clase círculo es una instancia de elipse y una instancia de figura. Ahora, supongamos que en nuestro lenguaje orientado a objetos declaramos un objeto de tipo figura del que no podemos crear instancias e invocamos su método darÁrea. ¿Cómo resuelve esto el compilador en nuestro lenguaje? Bueno, no lo resuelve sino hasta la ejecución dinámicamente. Y basado en la instancia que creamos allí, él llamará al método darÁrea de la clase que creó el objeto. Esto es lo que llamamos late binding. Y veamos una ilustración. Si se instancia el objeto figura como un elipse cuando se llame al método darÁrea dinámicamente, el programa va a llamar al método darÁrea que pertenece al elipse, no al de figura, y dará el resultado correcto. Todo esto gracias al polimorfismo que nos permiten los lenguajes orientados a objetos. Y bien, por ahora eso es todo acerca de herencia, pero todavía nos queda trabajar en construcción de modelos usando estas nuevas relaciones, así que quédate para los próximos videos y hasta la próxima. [MÚSICA] [MÚSICA] [MÚSICA] [MÚSICA] [AUDIO_EN_BLANCO] [AUDIO_EN_BLANCO] [AUDIO_EN_BLANCO]