sábado, 4 de noviembre de 2017

Curso de C#: interfaces

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.

Interfaces en C#


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.

 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);
}

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!
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) { ... }
}

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) { ... }
}

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);
}
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();

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();
}
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() { ... }
}
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"); }
}
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();
}
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
}
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();
}
Podrían existir clases que implementen IArmaDeGuerra como una torreta defensiva.

class TorretaDefensiva : IArmaDeGuerra
{
   
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() { ... }
}
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