Herencia y polimorfismo
Entender la herencia
El mecanismo de la herencia se utiliza mucho en programación orientada a objetos y es importante recordar su utilidad.
Heredar una clase influye en la "especialización" de determinados comportamientos y propiedades, aprovechando sus servicios básicos y, de esta manera, evitar redundancia de código.
C# solo permite una herencia por nivel (a diferencia de C++), aunque es posible heredar de una clase la cual sea heredada y así sucesivamente para construir una herencia de clases que parta de la más global hasta la más detallada.
Ejemplo de herencia de clases:
Una clase puede servir de "madre" para varias clases. El mejor ejemplo es, sin ninguna duda, System.Object, que es raíz de todos los tipos y, por tanto, de todas las clases de C#. Recordemos que la herencia de System.Object es implícita y no necesita ninguna definición particular.
Codificación de la clase de base y su heredada
El código de una clase define las reglas de su eventual herencia.
1. Prohibir la herencia
En primer lugar, ¿es deseable hacer una clase "heredable"? Si el análisis demuestra que no, entonces se debe utilizar la palabra clave sealed (cerrada) en la definición de la clase para prohibir cualquier herencia.
Sintaxis de declaración de una clase cerrada:
[visibilidad] sealed class NombreClase
{
//...
}
Ejemplo de clase cerrada:
public sealed class Director
{
//...
}
Como se muestra en la siguiente captura de imagen, el compilador rechaza una clase que hereda de Director:
La clase .NET System.String es una clase cerrada.
Una clase que no contiene la palabra clave sealed en su definición se considera como heredable.
2. Definir miembros heredables
Una clase de base define sus miembros "heredables" gracias a sus atributos de accesibilidad. De esta manera, las clases heredadas tendrán permiso de utilizar y redefinir los miembros de tipo protected y los de tipo public. Los miembros de tipo private seguirán siendo no accesibles.
3. Codificación de la herencia
Sintaxis de declaración de la herencia de una clase:
[visibilidad] [sealed] class NombreClaseHeredera: NombreClaseDeBase ...
Comunicación entre clase de base y clase heredada
1. Los constructores
Cuando se instancia una clase heredada, se llama al constructor de su clase de base, antes que al suyo. A continuación se muestra un fragmento de código seguido del resultado en la consola, que lo atestigua.
using System;
namespace Cap6
{
class Program
{
static void Main(string[] args)
{
new Prueba();
}
class ClasePadre
{
public ClasePadre()
{
Console.WriteLine("Ctor ClasePadre");
}
public string PropClasePadre { get; set; }
}
class ClaseHija: ClasePadre
{
public ClaseHija()
{
Console.WriteLine("Ctor ClaseHija");
}
public string PropClaseHija { get; set; }
}
class Prueba
{
public Prueba()
{
Console.WriteLine("Creación objeto ClaseHija");
ClaseHija dev = new ClaseHija();
}
}
}
}
Salida correspondiente...
Ejercicio
1. Enunciado
Crear una nueva solución de tipo Consola.
Añada una clase CuentaBancaria, que representa una cuenta bancaria con las siguientes propiedades:
-
Titular (string).
-
Número (entero que contiene un valor único asignado en la instanciación; el primer número de cuenta es 1000).
-
Saldo (double).
La clase también incluye los siguientes métodos:
-
Credito (permite ingresar dinero en la cuenta).
-
Debito (permite retirar dinero de la cuenta).
-
ConsultaSaldo (muestra toda la información de la cuenta).
Añada una clase CuentaBancariaRemunerada que representa una cuenta bancaria remunerada que hereda de la clase creada anteriormente y, por tanto, el constructor recibirá como argumentos el nombre del titular y el porcentaje de remuneración de la cuenta.
Redefina el método Credito de CuentaBancariaRemunerada para que la cantidad ingresada aumente en el porcentaje de remuneración definido en el constructor. No es necesario soñar, este funcionamiento bancario atípico es solo para simplificar el ejercicio.
Codifique en el Main una secuencia que permita comprobar el funcionamiento de las clases.
2. Corrección
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace LabCuentaBancaria
{
class Program ...
Las clases abstractas
Puede suceder que una clase de base contenga métodos imposibles de implementar porque en su nivel en la herencia no disponga de la información necesaria.
Por ejemplo, una clase de base FormaGeometrica ofrece un método virtual Diseñar. A continuación, esta clase se heredada por las clases Triangulo, Rectangulo y Circulo, que van a implementar cada una su propio método Diseñar.
Por lo tanto, la implementación del método Diseñar en la clase de base FormaGeometrica no tiene ningún sentido porque cada forma es específica y, al nivel de FormaGeometrica, esta forma es abstracta.
Entonces, en este caso, ¿por qué definir el método Diseñar en FormaGeometrica?
Esto está relacionado con el polimorfismo. Imagine que está construyendo una aplicación de diseño y que esta aplicación gestiona una serie de formas geométricas cuidadosamente definidas por el usuario. Puede gestionar una lista de objetos de tipo Triangulo, una lista de objetos de tipo Rectangulo, etc. Buena suerte, porque esto será bastante duro. Construyendo una lista de objetos de tipo FormaGeometrica, puede rellenarla de objetos de tipos Triangulo, Rectangulo y Circulo. ¿Por qué? Porque heredan los tres de la clase de base FormaGeometrica y cualquier clase derivada puede implícitamente convertirse en un objeto de su clase de base....
Los métodos de extensión
Para explicar el interés de los métodos de extensión, nada mejor que un caso concreto. En su aplicación, recibe variables de tipo String y normalmente necesita saber si su contenido son expresiones de valores numéricos.
Este tipo de operaciones no existe en la clase String y como no tiene su código fuente, no la puede añadir.
No puede construir sin más una clase heredada StringEx, porque la clase String es cerrada. Incluso si no lo fuera, hubiera necesitado sustituir todos los tipos String por StringEx en las clases de su proyecto, lo que es fastidioso.
Última solución: escribir el método y ubicarlo en una clase, preferiblemente de tipo static (comúnmente llamada un helper), que utilizará en las ocasiones adecuadas. Este método, muy utilizado, sigue siendo menos práctico que una implementación nativa de su operación en la clase string.
Existe una funcionalidad en C# que permite añadir "sobre la marcha" métodos a los tipos compilados justo en el contexto de su aplicación. No es necesario tener el código fuente del tipo (clase, estructura o interfaz) para añadir el método que le falta; los métodos de extensión sirven para ofrecerle una nueva flexibilidad de codificación. En la parte del código cliente, no hay ninguna diferencia entre la llamada...
El polimorfismo
1. Entender el polimorfismo
En programación orientada a objetos, el polimorfismo permite a una clase heredada presentarse a una operación como su clase de base o como una de sus interfaces. Gracias a la virtualización de los métodos, la operación que llama al método básico se "enruta" en la clase heredada. De esta manera se obtiene una "especialización" de la operación.
Cualquier referencia a una instancia de clase derivada se puede convertir implícitamente en una referencia a una instancia de tipo de su clase de base.
En C#, la herencia y sus palabras clave virtual y override que se han detallado en el capítulo anterior por una parte permiten a la clase de base definir los métodos que se pueden especializar por parte de sus heredadas y, por otro lado, a su o sus heredadas tener en cuenta los métodos para especializarlos.
En C#, cada tipo es "polimorfo". En efecto, se puede considerar como su propio tipo o como el de su clase de base y, por encadenamientos sucesivos, como el tipo raíz de todos los tipos, a saber System.Object.
2. Explotación del polimorfismo
La mejor programación es la que favorece la independencia entre los módulos. Una arquitectura compuesta de loosely coupled modules es la más recomendada, porque permite la evolución de las capas independientemente del funcionamiento global....