Interfaces
En teoría de orientación a objetos, la interfaz de una
clase es todo lo que podemos hacer con ella.
A efectos prácticos: todos los métodos, propiedades de la clase conforman su interfaz.
Nota.
(Las variables públicas también
formarían parte de la interfaz, pero no es recomendable declarar variables
públicas. Siempre acceder a ellas a través de propiedades).
No tendría sentido
establecer una relación de herencia entre conjuntos completamente distintos,
por más que muchos de sus miembros fueran a ser comunes. Por ejemplo, no
estaría bien heredar una clase tarjeta de crédito y otra clase cuenta corriente
de una clase banco. Por más que en todos ellos puedan hacerse ingresos o
reintegros, está claro que ni las tarjetas de crédito ni las cuentas corrientes
son bancos. En resumen, no hay que basarse únicamente en la funcionalidad para
establecer una relación de herencia entre clases.
Es en este último caso
donde juegan un papel importante las interfaces. Una interfaz es la definición
de un conjunto de miembros que serán comunes entre clases que serán (o no)
completamente distintas. La interfaz conoce cómo será la funcionalidad de
cualquier clase que la implemente pero, no podrá conocer los detalles de esa
implementación en cada una de esas clases, de modo que una interfaz no
puede implementar nada de código, sino que, sólo puede describir un conjunto de
miembros.
La interfaz ICloneable
puede hacer copias de objetos de tipo referencia mediante el método Clone. Está
claro que serán muchas las clases que puedan hacer copias de sí mismas, pero
también está claro que, aparte de este detalle, dichas clases no tienen por qué
tener nada más en común.
Un ejemplo: las clases
Factura y ArrayList pueden hacer copias de sí mismas, pero una factura no tiene
nada más que ver con un ArrayList. Sin embargo, si queremos escribir un método
que se ocupara de hacer la copia de uno de estos objetos, cualquiera que sea,
tendremos el problema de que habría que escribir una sobrecarga para cada uno
de los tipos que queramos copiar. Si el método se llama Copiar, habría que
escribir dos sobrecargas de él: Copiar (Factura f) y Copiar (ArrayList a).
También sería posible escribir un sólo método usando como argumento un tipo
object, es decir, Copiar (object o), pero dentro de él tendríamos que
determinar cuál es, exactamente, el tipo del objeto y hacer la conversión en
cada caso para poder invocar el método Clone ( return f.Clone(); o
bien return a.Clone() ), pues el tipo object no contiene ninguna
definición para este método. Pero, si se implementa la interfaz ICloneable en
ambas clases será posible escribir un único método con un argumento que
especifique cualquier objeto que implemente dicha interfaz, Copiar(ICloneable
ic), como el objeto ic sí tiene un método Clone que se puede invocar
directamente, independientemente que sea una Factura o un ArrayList(return ic.Clone()). En
resumen, las interfaces sirven para poder agrupar funcionalidades.
Los lenguajes .NET sólo
permiten herencia simple. Una clase se puede derivar de otra clase, pero no de
varias; sin embargo, sí permite implementar en una misma clase tantas
interfaces como queramos.
Otro ejemplo: si tenemos
varias clases distintas que deben ofrecer la capacidad de presentar sus datos
en la consola. No tendría mucho sentido utilizar una clase base para todas
ellas, porque cada una será distinta de la otra. Se trata de otro caso más donde
es más adecuado crear un interfaz que en el ejemplo, llamaremos IPresentable.
Especificará la existencia de un método, al cual llamaremos Presentar:
interface IPresentable
{
void Presentar();
}
La Interface no implementa
el código del método, se limita a indicar que debe existir un método Presentar
en todas las clases que la implementen. Las interfaces siempre empiezan por la
letra I. No lo exige el compilador, es una convención de codificación. A
continuación se muestran dos clases distintas pero que implementarán esta
interfaz:
class Triangulo:IPresentable
{
public double Base;
protected double Altura;
public Triangulo(double Base, double altura)
{
this.Base=Base;
this.Altura=altura;
}
public double Area
{
get { return Base*Altura/2; }
}
public void Presentar()
{
Console.WriteLine("Base del triángulo: {0}",
Base);
Console.WriteLine("Altura del triángulo:
{0}", Altura);
Console.WriteLine("Área del triángulo: {0}", Area);
}
}
class Proveedor:IPresentable
{
public string Nombre;
public string Apellidos;
public string Direccion;
public Proveedor(string nombre, string apellidos, string direccion)
{
Nombre=nombre;
Apellidos=apellidos;
Direccion=direccion;
}
public void Presentar()
{
Console.WriteLine("Nombre: {0}", Nombre);
Console.WriteLine("Apellidos: {0}",
Apellidos);
Console.WriteLine("Dirección: {0}",
Direccion);
}
}
Un proveedor no tiene nada
que ver con un triángulo. De ahí el motivo por el que la interfaz no
implementa el método. A continuación el código de ejemplo que utiliza el
interfaz:
class EjemploInterfaces
{
static void Main()
{
Triangulo t=new Triangulo(10,5);
Proveedor p=new Proveedor("Frutas","Frutas
José", "Tienda");
Console.WriteLine("objetos creados");
Console.WriteLine("Pulsar
ENTER para invocar VerDatos(triangulo)");
Console.ReadLine();
VerDatos(t);
Console.WriteLine();
Console.WriteLine("Pulsar ENTER para invocar
VerDatos(proveedor)");
Console.ReadLine();
VerDatos(p);
}
static void VerDatos(IPresentable IP)
{
IP.Presentar();
}
}
El método VerDatos
reconoce cualquier objeto que implemente la interfaz IPresentable,
independientemente de cuál sea su clase.
¿Qué es la interfaz de una clase?
Dada la
siguiente clase:
class Contenedor
{
public int Quitar();
public void Meter(int v);
private bool EstaRepetido(int v);
}
Su interfaz está formada por los métodos Quitar y Meter. El método EstaRepetido no forma parte de la
interfaz de dicha clase, ya que es privado.
La interfaz de una clase define su comportamiento. Dado un objeto
de la clase Contenedor se puede llamar
al método Quitar y al métdo Meter pero no es posible llamar al método EstaRepetido. De este modo, toda clase tiene una interfaz que
define, qué es posible hacer con los objetos de dicha clase.
Interfaces idénticas no
significa que sean clases intercambiables
class Contenedor
{
public int Quitar() { ... }
public void Meter (int v) { ... }
}
class OtroContenedor
{
public int Quitar() { ... }
public void Meter (int v) { ... }
}
Su interfaz
es la misma: con ambas clases podemos hacer lo mismo.
public void Maneja_Contenedor (Contenedor c)
{
// Hacer cosas con c como
int i = c.Quitar();
c.Meter(10);
}
El método recibe un Contenedor y
opera con él. Pero como las interfaces Contenedor y OtroContenedor son iguales,
si hacemos:
OtroContenedor oc = new OtroContenedor();
Maneja_Contenedor(oc);
No compilará, aunque
nosotros somos capaces leyendo el código de comparar la interfaz de ambas
clases, el compilador no puede hacerlo. Para el compilador Contenedor y OtroContenedor son dos clases totalmente distintas sin
ninguna relación. Por lo que un método que espera un Contenedor no puede aceptar
un objeto de la clase OtroContenedor.
El compilador no compara
las interfaces de las clases y no es debido a una imposibilidad técnica,
es porque no tiene sentido hacerlo. Las interfaces son idénticas por
casualidad. Si fuese posible llamar a Maneja_Contenedor con un objeto OtroContenedor, podría darse
este caso:
Se añade un
método público a la clase Contenedor.
Se modifica el método Maneja_Contenedor
para que llame a dicho método nuevo.
Eso es posible porque Maneja_Contenedor
espera un Contenedor como parámetro. La
llamada a Maneja_Contenedor(oc) donde oc
es OtroContenedor no tiene el
método nuevo que se añadió a Contenedor.
De este modo, dos clases con la misma interfaz no tienen relación alguna entre ellas y
por lo tanto no se pueden intercambiar.
Nota.
(Las variables públicas también
formarían parte de la interfaz, pero no es recomendable declarar variables
públicas. Siempre acceder a ellas a través de propiedades).
{
public int Quitar();
public void Meter(int v);
private bool EstaRepetido(int v);
}
{
public int Quitar() { ... }
public void Meter (int v) { ... }
}
class OtroContenedor
{
public int Quitar() { ... }
public void Meter (int v) { ... }
}
{
// Hacer cosas con c como
int i = c.Quitar();
c.Meter(10);
}
Maneja_Contenedor(oc);
Implementación de interfaces
Si Contenedor está
implementado usando un array en memoria y OtroContenedor está
implementando usando, algo diferente. La funcionalidad (la interfaz) es la
misma, lo que varía es la
implementación. Es por ello que en programación orientada a objetos
se dice que las interfaces son funcionalidades (o comportamientos) y las clases
representan implementaciones.
Pero puede darse el caso contrario: Que dos clases
representen dos implementaciones distintas de la
misma funcionalidad, sería absurdo que no las pudiéramos intercambiar.
Para que dicho intercambio sea posible C# (o cualquier lenguaje orientado a
objetos) permite separar la declaración de la interfaz de
su implementación (de su clase). Para ello se utiliza la palabra
clave interface:
interface IContenedor
{
int Quitar();
void Meter(int i);
}
{
int Quitar();
void Meter(int i);
}
Este código declara una interfaz IContenedor que declara los
métodos Quitar y Meter. Los métodos no
se declaran como public (en una interfaz la visibilidad no tiene sentido, ya
que todo es public) y que no se
implementan los métodos.
Las
interfaces son un concepto más teórico que real. No se pueden crear interfaces.
El siguiente código es un error:
IContenedor c = new IContenedor();
// Error: No se puede crear una interfaz!
// Error: No se puede crear una interfaz!
Si nos
dejara, y hacemos c.Quitar(). No puede llamarlo porque el método Quitar() no
está implementado. Pero es posible indicar explícitamente que una clase implementa una interfaz, es
decir proporciona implementación (código) a
todos y cada uno de los métodos (y propiedades) declarados en la interfaz:
class Contenedor : IContenedor
{
public int Quitar() { ... }
public void Meter(int i) { ... }
}
{
public int Quitar() { ... }
public void Meter(int i) { ... }
}
La clase Contenedor declara
explícitamente que implementa la interfaz IContenedor.
Así pues la clase debe proporcionar
implementación para todos los métodos de la interfaz. El siguiente código
es incorrecto porque le falta implementar el método quitar.
class Contenedor : IContenedor
{
public void Meter(int i) { ... }
}
{
public void Meter(int i) { ... }
}
Al crear una clase interfaz, obliga a implementar
ciertos métodos y al utilizar la clase, la interfaz dice que métodos se pueden
llamar.
La ventaja de las interfaces es que, si dos
clases implementan la misma interfaz, son
intercambiables. Es decir, en cualquier sitio donde se espere una instancia de
la interfaz puede pasarse una instancia de cualquier clase que implemente dicha
interfaz.
Podríamos
declarar nuestro método Maneja_Contenedor así:
void Maneja_Contenedor (IContenedor c)
{
// Cosas con c...
c.Quitar();
c.Meter(10);
}
{
// Cosas con c...
c.Quitar();
c.Meter(10);
}
La clave está en que el parámetro de Maneja_Contenedor está declarado como IContenedor, no como Contenedor u OtroContenedor, con esto se indica que el método Maneja_Contenedor()
trabaja con cualquier objeto de cualquier clase que implemente IContenedor.
Como Contenedor
y OtroContenedor implementan la interfaz IContenedor el siguiente código es
válido:
Contenedor c = new Contenedor();
Maneja_Contenedor (c); // Maneja_Contenedor espera IContenedor y Contenedor implementa IContenedor
OtroContenedor oc = new OtroContenedor();
Maneja_Contenedor (oc); // Maneja_Contenedor espera IContenedor y OtroContenedor implementa IContenedor
// esto también es válido:
IContenedor ic = new Contenedor();
IContenedor ic2 = new OtroContenedor();
Maneja_Contenedor (c); // Maneja_Contenedor espera IContenedor y Contenedor implementa IContenedor
OtroContenedor oc = new OtroContenedor();
Maneja_Contenedor (oc); // Maneja_Contenedor espera IContenedor y OtroContenedor implementa IContenedor
// esto también es válido:
IContenedor ic = new Contenedor();
IContenedor ic2 = new OtroContenedor();
Cuando conviene utilizar interfaces
En general
siempre que se tenga o se prevea tener más de una clase para hacer lo mismo es
conveniente utilizar interfaces. Es mejor implementarlo aunque luego no se
utilice.
A la hora
del diseño hay que pensar en la interfaz antes que en la clase en sí, pensar en lo que debe hacerse en
lugar de pensar en cómo debe
hacerse. Utilizar interfaces permite a posteriori cambiar una clase por
otra que implemente la misma interfaz y poder integrar la nueva clase de forma
mucho más fácil (sólo se modificará en el lugar donde se instancian los objetos
pero el resto de código queda igual).
Segregación de interfaces
Si por
ejemplo tuviéramos un sistema que debe trabajar con varios vehículos, entre
ellos aviones y coches, declaramos la
siguiente interfaz:
interface IVehiculo
{
void Acelerar(int kmh);
void Frenar();
void Girar(int angulos);
void Despegar();
void Aterrizar();
}
{
void Acelerar(int kmh);
void Frenar();
void Girar(int angulos);
void Despegar();
void Aterrizar();
}
Se
implementa la clase avión.
class Avion : IVehiculo
{
public void Acelerar(int kmh) { ... }
public void Frenar() { ... }
public void Girar (int angulos) { ... }
public void Despegar() { ... }
public void Aterrizar() { ... }
}
{
public void Acelerar(int kmh) { ... }
public void Frenar() { ... }
public void Girar (int angulos) { ... }
public void Despegar() { ... }
public void Aterrizar() { ... }
}
Se
implementa la clase coche.
class Coche : IVehiculo
{
public void Acelerar(int kmh) { ... }
public void Frenar() { ... }
public void Girar (int angulos) { ... }
public void Despegar() {throw new NotImplementedException("Coches no vuelan"); }
public void Aterrizar(){throw new NotImplementedException("Coches no vuelan"); }
}
{
public void Acelerar(int kmh) { ... }
public void Frenar() { ... }
public void Girar (int angulos) { ... }
public void Despegar() {throw new NotImplementedException("Coches no vuelan"); }
public void Aterrizar(){throw new NotImplementedException("Coches no vuelan"); }
}
La interfaz
IVehiculo tiene demasiados métodos y no define el comportamiento de todos los
vehículos, pues no todos los vehículos despegan y aterrizan. En este caso es
mejor dividir la interfaz en dos:
interface IVehiculo
{
void Acelerar(int kmh);
void Frenar();
void Girar (int angulos);
}
interface IVehiculoVolador : IVehiculo
{
void Despegar();
void Aterrizar();
}
{
void Acelerar(int kmh);
void Frenar();
void Girar (int angulos);
}
interface IVehiculoVolador : IVehiculo
{
void Despegar();
void Aterrizar();
}
IVehiculoVolador deriva de IVehiculo (hay una relación
de herencia entre IVehiculoVolador y IVehiculo), una
clase que implemente IVehiculoVolador debe implementar también IVehiculo
forzosamente. Por lo tanto todos los vehículos voladores son también
vehículos.
Ahora sí es
posible hacer que la clase Coche pueda implementar IVehiculo y la clase Avion
pueda implementar IVehiculoVolador (y por lo tanto también IVehiculo). Si un
método Maneja_Interfaz recibe un objeto IVehiculoVolador
puede usar métodos tanto de IVehiculoVolador como de IVehiculo.
void Maneja_Interfaz(IVehiculoVolador vv)
{
vv.Acelerar(10); // Acelerar es de IVehiculo y IVehiculoVolador deriva de IVehiculo
vv.Despegar(); // Despegar es de IVehiculoVolador
}
{
vv.Acelerar(10); // Acelerar es de IVehiculo y IVehiculoVolador deriva de IVehiculo
vv.Despegar(); // Despegar es de IVehiculoVolador
}
Si un método recibe un IVehiculo no puede llamar a métodos de
IVehiculoVolador. Pues todos los vehículos voladores son vehículos pero lo
inverso no es cierto, no todos los vehículos
son vehículos voladores.
Siempre que
haya segregación no tiene por qué haber herencia de interfaces. Veamos el
siguiente ejemplo.
interface IArmaDeGuerra
{
void Apuntar();
void Disparar();
}
{
void Apuntar();
void Disparar();
}
Podrían
existir clases que implementen IArmaDeGuerra como
una torreta defensiva.
class TorretaDefensiva : IArmaDeGuerra
{
public void Apuntar() { ... }
public void Disparar() { ... }
}
{
public void Apuntar() { ... }
public void Disparar() { ... }
}
Aunque también hay vehículos que pueden ser a la vez
armas de guerra, como un tanque. En este caso es posible implementar una clase de más de una interfaz a la
vez. Para ello debe implementar todos los métodos de todas la
interfaces.
class Tanque : IVehiculo, IArmaDeGuerra
{
public void Acelerar(int kmh) { ... }
public void Frenar() { ... }
public void Girar (int angulos) { ... }
public void Apuntar() { ... }
public void Disparar() { ... }
}
{
public void Acelerar(int kmh) { ... }
public void Frenar() { ... }
public void Girar (int angulos) { ... }
public void Apuntar() { ... }
public void Disparar() { ... }
}
Ahora, si un
método Maneja_Interfaz () recibe un IVehiculo
se le puede pasar un objeto Tanque y si otro método Maneja_Interfaz2
recibe un IArmaDeGuerra también se le puede pasar un objeto Tanque. En este caso los tanques se comportan como
vehículos y como armas de guerra a la vez.
No hay comentarios:
Publicar un comentario