Clase base, interface y clase abstracta, ¿amigas o enemigas? – Lo que un Dummy debe saber
Introducción
En esta ocasión, me gustaría tratar aspectos relacionados con la programación orientada a objetos y algunos aspectos básicos generales.
Tengo en mente escribir (y si el tiempo me lo permite) unas cuantas entradas relacionadas con diferentes temas, pero debo empezar por algún sitio, y he creído conveniente hacerlo por la base de todos ellos, así que empiezo por esta para que los diferentes conocimientos que quiero o persigo explicar en futuras entradas tengan sentido y tomen forma.
Seguramente muchos ya conozcan lo que voy a contar, y estoy convencido de que incluso mejor que yo seguramente, así que quiero comentar aspectos relacionados con las clases base, interfaces y clases abstractas… porque… ¿son la misma cosa o no?. ¿Son amigas o enemigas? ¿Sabemos diferenciarlas o relacionarlas adecuadamente?. ¿Cuando debemos elegir una u otra?.
Antes de abordar la parte práctica del tema repasaremos un poco la parte teórica de qué es cada cosa, diferentes aplicaciones, implicaciones y demostraciones prácticas de lo que comente al respecto en cada caso, y finalizar con un pequeño desglose muy breve diferenciando entre clases abstractas e interfaces.
¡Vamos allá!.
Clase base
Una clase base es o puede ser cualquier tipo de clase. Es decir, para que una clase sea una clase base, no necesita nada especial.
Una clase base como tal, puede ser heredada (que es lo más habitual) o instanciada (si queremos acceder a la clase base de forma directa).
Sólo existe una restricción para heredar de ella, y es que la clase base esté sellada (sealed), en cuyo caso no podremos heredar de esa clase base.
Otra ventaja que tienen las clases base, es que podemos igualmente sobreescribir o reemplazar sus métodos y funciones siempre que éstas estén decoradas con virtual y las clases que heredan los métodos y funciones de la clase base, decoradas con override para sobreescribirlas (en C++ creo recordar ahora mismo de memoria que no es necesario este último detalle).
Un ejemplo típico en .NET de este tipo de clases es la clase base System.Exception.
No obstante, vamos a abordar un pequeño ejemplo que nos ayude a comprender esto un poco más.
1: namespace OOP
2: {
3:
4: public class Saludo
5: {
6: public virtual string GetSaludo()
7: {
8: return "Hello";
9: }
10:
11: public string GetSaludoCompleto(string name)
12: {
13: return string.Format("{0} {1}", GetSaludo(), name);
14: }
15: }
16:
17: public class SaludoSpanish : Saludo
18: {
19: public override string GetSaludo()
20: {
21: return "Hola";
22: }
23: }
24:
25: }
El código de llamada en un formulario Windows para cualquiera de la clases que implementa la interfaz es el que se indica a continuación:
1: OOP.SaludoSpanish saludo = new OOP.SaludoSpanish();
2: MessageBox.Show(saludo.GetSaludo() +
3: Environment.NewLine +
4: saludo.GetSaludoCompleto("Geeks.ms"));
¿Qué información obtendremos por pantalla?.
Hola Hola Geeks.ms |
Aún y así, ¿hay alguna forma de crear una función GetSaludoCompleto en la clase SaludoSpanish que reemplace a la función GetSaludoCompleto de la clase base Saludo?.
Para ver que ocurre, escribiremos el siguiente código en la clase SaludoSpanish:
1: public string GetSaludoCompleto(string name)
2: {
3: return string.Format("{0} {1}, hablas español.", GetSaludo(), name);
4: }
En este caso, el IDE de Visual Studio 2010 nos devolverá un warning que nos indicará que estamos ocultando GetSaludoCompleto en la clase base Saludo con GetSaludoCompleto de la clase SaludoSpanish, y que utilicemos una nueva palabra clave si queríamos ocultarlo.
Pese a este aviso, el resultado que obtendremos por pantalla en este caso, sí será diferente:
Hola Hola Geeks.ms, hablas español. |
De todos los modos, ¿cómo hacerlo bien?, ¿cómo resolverlo?.
Bastará con decorar la función GetSaludoCompleto de la clase SaludoSpanish con new de la siguiente manera:
1: public new string GetSaludoCompleto(string name)
2: {
3: return string.Format("{0} {1}, hablas español.", GetSaludo(), name);
4: }
De esta manera, estaremos ocultando GetSaludoCompleto en la clase base Saludo por la función de mismo nombre pero en la clase SaludoSpanish.
El resultado que obtendremos en pantalla será:
Hola Hola Geeks.ms, hablas español. |
Ahora bien, giremos un poco más la tuerca y vayamos más lejos…
Imaginemos ahora que escribimos el siguiente código con respecto a la última modificación de nuestra clase SaludoSpanish:
1: OOP.Saludo saludo = new
2: OOP.SaludoSpanish();
3: MessageBox.Show(saludo.GetSaludo() +
4: Environment.NewLine +
5: saludo.GetSaludoCompleto("Geeks.ms"));
¿Qué información crees ahora que obtendremos ahora por pantalla?.
Hola Hola Geeks.ms |
En este caso, tenemos un objeto de tipo Saludo que referencia a SaludoSpanish.
1: OOP.Saludo saludo = new OOP.SaludoSpanish();
GetSaludo será o pertenecerá en este caso a la clase SaludoSpanish ya que está sobreescrita.
Sin embargo, a la hora de acceder a GetSaludoCompleto, no está accediendo a la función de la clase SaludoSpanish.
Es más, en ningún momento estamos accediendo a la clase SaludoSpanish, sino a una clase base Saludo que referencia a la clase SaludoSpanish.
Esto significa que accedemos en todo momento a la clase base Saludo modificando los miembros, funciones y métodos sobreescritos (referenciados) en la clase SaludoSpanish.
De esta manera, accedemos a la función sobreescrita GetSaludo de la clase SaludoSpanish, pero a GetSaludoCompleto de la clase base Saludo, que corresponde con la declaración que tenemos arriba.
Clase abstracta
Muchas veces se confunde con clase base.
No es que sean muy diferentes, pero ojo, tampoco son la misma cosa. Es importante aclarar este aspecto.
De hecho, una clase abstracta puede contener lógica y funcionalidad, algo que es coincidente con una clase base.
Sin embargo, no es posible instanciar una clase abstracta, mientras que en el caso de una clase base sí tal y como comenté anteriormente.
Es decir, para utilizar una clase abstracta, no tenemos más remedio que heredar de ella.
No obstante, podemos sobreescribir los miembros, métodos y funciones de una clase abstracta, e incluso extender la clase que herede de ella con las particularidades que consideremos oportunas.
1: namespace OOP
2: {
3: public abstract class Saludo
4: {
5: public abstract string GetSaludo();
6: public string GetSaludoCompleto(string name)
7: {
8: return string.Format("{0} {1}", GetSaludo(), name);
9: }
10: }
11:
12: public class SaludoSpanish : Saludo
13: {
14: public override string GetSaludo()
15: {
16: return "Hola";
17: }
18: }
19:
20: public class SaludoEnglish : Saludo
21: {
22: public override string GetSaludo()
23: {
24: return "Hello";
25: }
26: }
27: }
El código de llamada en un formulario Windows para cualquiera de la clases que implementa la interfaz es el que se indica a continuación:
1: OOP.SaludoEnglish saludo = new OOP.SaludoEnglish();
3: MessageBox.Show(saludo.GetSaludo() +
4: Environment.NewLine +
5: saludo.GetSaludoCompleto("Geeks.ms"));
En este caso, la salida por pantalla será la siguiente:
Hello Hello Geeks.ms |
Aquí podríamos aplicar los ejemplos y casuísticas que hemos mencionado anteriormente para las clases bases.
Interfaz
Quizás el uso y diferencia de las interfaces esté más claro que ninguna de las dos anteriores.
En primer lugar, debemos asociar interfaz con comportamiento o mejor aún, con contrato.
Una interfaz expone un contrato que debe cumplirse.
Un ejemplo típico del uso de contratos son los Servicios WCF en .NET.
Básicamente se trata de representar un contrato que debe cumplir cualquier clase que implemente la interfaz. Así de simple y así de sencillo.
Si creamos una interfaz ISaludo con una función GetSaludo, crearemos una clase SaludoSpanish que implemente la interfaz ISaludo y que devuelva en su función GetSaludo un «Hola«.
Si creamos una clase SaludoEnglish que implementa la interfaz ISaludo y escribimos la función GetSaludo, ésta deberá devolver «Hello«.
Es decir, creamos una interfaz ISaludo que especifica un contrato, en este caso el método GetSaludo.
Este método será implementado, extendido en cada una de las clases que implementen la interfaz agregando en este caso un saludo en el idioma correspondiente.
Desde el punto de vista de programación, una interfaz reúne un conjunto de miembros abstractos.
La puntualización es que en una interfaz sólo se definen los miembros, y nunca se implementa nada.
Los miembros de una interfaz (todos) son públicos por defecto.
1: namespace OOP
2: {
3:
4: public interface ISaludo
5: {
6: string GetSaludo();
7: }
8:
9: public class SaludoSpanish : ISaludo
10: {
11: public string GetSaludo()
12: {
13: return "Hola";
14: }
15: }
16:
17: public class SaludoEnglish : ISaludo
18: {
19: public string GetSaludo()
20: {
21: return "Hello";
22: }
23: }
24: }
El código de llamada en un formulario Windows para cualquiera de la clases que implementa la interfaz es el que se indica a continuación:
1: OOP.SaludoSpanish saludo = new OOP.SaludoSpanish();
2: MessageBox.Show(saludo.GetSaludo());
También podríamos haber declarado el siguiente uso:
1: OOP.ISaludo saludo = new OOP.SaludoSpanish();
2: MessageBox.Show(saludo.GetSaludo());
Clase abstracta vs Interfaz
Adicionalmente, me gustaría detenerme un poco en la diferencia entre una clase abstracta y una interfaz.
Veo con mucha frecuencia que muchas veces no queda del todo claro.
Y es que mucha gente no conoce bien la diferencia entre una clase abstracta y una interfaz, o en su caso, las confunde… así que permitidme que me detenga aquí un momento para enumerar las coincidencias y diferencias entre ambas.
Interfaz
Como hemos comentado, una interfaz se encarga de definir comportamientos, contratos.
En cada interfaz, podemos declarar métodos, funciones, eventos, delegados o propiedades.
Todos los miembros de una interfaz son públicos y abstractos.
En otro orden de cosas, todo lo que declaremos en una interfaz, deberá ser implementado en las clases que implementen la interfaz.
Una clase por su parte, puede implementar más de una interfaz.
Finalmente, una interfaz no posee estado (data members) o implementación alguna (funciones y métodos).
Clase abstracta
Una clase abstracta puede contener funciones y métodos que serán utilizados por las clases que implementen la clase abstracta.
En cada clase abstracta, podemos declarar métodos, funciones, eventos, delegados, propiedades y variables.
Una clase abstracta puede incluir funcionalidad, al contrario que una interfaz.
Una clase sólo puede implementar una única clase abstracta, ya que no existe soporte para herencia múltiple.
Independientemente de todo lo comentado hasta ahora, una clase que herede de una clase abstracta no tiene porqué implementar ningún método abstracto de la misma ya que como clase derivada, es en sí misma una clase abstracta.
Finalmente, una clase abstracta puede contener estados (data members) e implementaciones (funciones y métodos).
En términos de velocidad o rendimiento, una clase abstracta es más rápida que una interfaz, ya que la clase que implementa la interfaz, debe preparar todas las definiciones de la misma.
3 Responsesso far
Excelente resumen, la información muy precisa, clara y consistente ! Me ha quedado muy claro, Gracias !
Muchas gracias
Excelente por tamaña explicación. A quedado todo muy claro. Gracias por compartir.