Interfaces
En resumen: Una interfaz es un contrato: una lista de métodos sin cuerpo que toda clase que la implementa tiene que escribir; una clase puede implementar varias interfaces.
Problema
Salarios y facturas
A final de mes la empresa paga dos tipos de cosas: los salarios de los empleados y las facturas de los proveedores. Quieres un solo array y un solo bucle: a quién se paga, cuánto, y el total al final.
Un array Employee[] no sirve: una factura no es un empleado. Hacer que herede de Employee está mal. La frase «una factura es un empleado» no es cierta, y además la factura tendría un nombre, un salario y un aumento.
Una clase padre común Payment tampoco sirve. Manager ya tiene una clase padre, Employee, y una clase solo puede tener una. Los empleados y las facturas no tienen nada en común, salvo que se pueden pagar.
La idea
Una interfaz: un contrato «puede hacer»
Una interfaz es una lista de métodos sin cuerpo. Dice qué tiene que saber hacer una clase, pero no cómo:
interface IPayable{ int AmountDue();
string Payee();}class Invoice : IPayable{ public string Supplier { get; private set; } public int Amount { get; private set; }
public Invoice(string supplier, int amount) { Supplier = supplier; Amount = amount; }
public int AmountDue() { return Amount; }
public string Payee() { return Supplier; }}
Invoice power = new Invoice("Compañía eléctrica", 340);Console.WriteLine(power.Payee() + ": " + power.AmountDue() + " €");Compañía eléctrica: 340 €Una interfaz se implementa con los mismos dos puntos que la herencia. Los métodos se escriben sin override: no hay nada que sobrescribir, porque la interfaz no tiene versiones propias.
En C# los nombres de las interfaces empiezan por I: IPayable, IComparable, IDisposable. Así class Invoice : IPayable muestra enseguida que los dos puntos son de una interfaz.
Los métodos de una interfaz siempre son public. Son una promesa a todos, así que en la clase también tienen que ser public.
Míralo
Un solo bucle para todo lo que se paga
El empleado también acepta el contrato. Su importe es el salario mensual:
class Employee : IPayable{ // ... Name, Salary, el constructor, MonthlyPay()
public int AmountDue() { return MonthlyPay(); }
public string Payee() { return Name; }}Ahora tanto los empleados como las facturas son IPayable. Una interfaz es un tipo: puede ser el tipo de una variable, de un parámetro o de un array.
IPayable[] payments ={ new Employee("Juan", 2000), new Manager("María", 3000, 500), new Invoice("Compañía eléctrica", 340), new Invoice("Casero", 1800)};int total = 0;foreach (IPayable p in payments){ Console.WriteLine(p.Payee() + ": " + p.AmountDue() + " €"); total += p.AmountDue();}Console.WriteLine("Total: " + total + " €");Juan: 2000 €María: 3500 €Compañía eléctrica: 340 €Casero: 1800 €Total: 7640 €El gerente no escribe nada nuevo. De Employee hereda tanto el contrato como AmountDue(). Y AmountDue() llama a MonthlyPay(), que el gerente ha sobrescrito. Por eso María recibe 3500 €.
IPayable[] payments ={ new Employee("Juan", 2000), new Manager("María", 3000, 500), new Invoice("Compañía eléctrica", 340)};int total = 0;foreach (IPayable p in payments){ total += p.AmountDue();}Console.WriteLine("Total: " + total + " €");Paso a paso, en palabras
- El array es de tipo IPayable. Las flechas apuntan a tres clases, pero solo llegan a su contrato.
- total empieza en 0.
- p apunta al empleado: AmountDue() devuelve el salario, 2000.
- El gerente es un empleado, así que también se le puede pagar. Salario más bono: 3500.
- La factura no es un empleado, pero cumple el mismo contrato: 340.
- El bucle no conoce las clases. Solo sabe que a cada uno se le puede pagar.
Pruébalo
¿Cuál es el importe?
¿Qué imprimirá el programa? Las clases son de la tarjeta de arriba.
IPayable p = new Manager("María", 3000, 500);Console.WriteLine(p.AmountDue());Manager es un Employee, y Employee implementa el contrato. Así que el gerente también lo implementa, sin escribirlo. La llamada va al método sobrescrito del gerente: 3000 + 500.
La idea
Una clase padre, muchos contratos
Contabilidad también quiere las facturas en formato CSV: una línea «proveedor;importe» por cada una. Ese es un segundo contrato. Una clase puede implementar todas las interfaces que quiera, separadas por comas:
interface IExportable{ string ToCsv();}
class Invoice : IPayable, IExportable{ // ... como antes
public string ToCsv() { return Supplier + ";" + Amount; }}IExportable[] forAccounting = { new Invoice("Compañía eléctrica", 340), new Invoice("Casero", 1800) };foreach (IExportable e in forAccounting){ Console.WriteLine(e.ToCsv());}Compañía eléctrica;340Casero;1800Si la clase tiene a la vez una clase padre y interfaces, la clase padre va primero:
class Manager : Employee, IExportableEl primer nombre después de los dos puntos puede ser una clase. Todos los demás son interfaces.
Hay una sola clase padre y muchos contratos. Así, una clase cuya clase padre ya está ocupada puede entrar igualmente en un array común con clases completamente distintas.
Pruébalo
Encuentra el error
Elige la línea con el error.
public. Los métodos de una interfaz se prometen a todos, y sin modificador el método queda oculto. El compilador dice que Invoice no implementa IPayable.AmountDue(), porque el método no es public.Pruébalo
¿Qué falta?
class Invoice IPayable
{
// ...
}IPayable con una I delante muestra que es una interfaz.La idea
¿Clase abstracta o interfaz?
Las dos son clases padre de las que no puedes crear objetos, y las dos ponen obligaciones a las clases de debajo. La diferencia está en lo que dicen:
| Clase abstracta | Interfaz | |
|---|---|---|
| Dice | «X es un Y» | «X puede» |
| Cuántas por clase | una | todas las que quieras |
| Campos y constructor | sí | no |
| Código común | sí | casi nada |
- Una clase abstracta, cuando las clases de debajo son un mismo tipo de cosa y comparten código y datos: las figuras tienen un nombre y una descripción, los empleados tienen un nombre y un salario.
- Una interfaz, cuando las clases son distintas pero necesitan una capacidad común: un empleado y una factura «se pueden pagar», un pájaro y un avión «pueden volar».
Employee es la clase padre del gerente y del becario, y a la vez implementa IPayable.Más precisamente, para curiosos
- Desde C# 8 una interfaz puede tener un método por defecto, con cuerpo. Las clases lo reciben ya hecho, y también pueden escribir el suyo. Así puedes añadir un método nuevo a una interfaz sin romper las clases que ya la implementan. Un método así se llama a través de una variable del tipo de la interfaz (
IFlyable f), no a través deBird:
interface IFlyable{ void Fly();
void Glide() { Console.WriteLine("Planea por el aire"); }}
class Bird : IFlyable{ public void Fly() { Console.WriteLine("El pájaro bate las alas"); }}
class Plane : IFlyable{ public void Fly() { Console.WriteLine("El avión despega"); }}
IFlyable[] flyers = { new Bird(), new Plane() };foreach (IFlyable f in flyers){ f.Fly(); f.Glide();}El pájaro bate las alasPlanea por el aireEl avión despegaPlanea por el aire- Una interfaz no puede tener campos. Puede tener propiedades, pero solo como promesa:
int Amount { get; }dice «toda clase tendrá esta propiedad».
Ejercicio
Contratos
Resuelve las tareas en tu editor.
1. Claxon. Una interfaz IHonkable con un método Honk(). SimpleCar del módulo «Clases y objetos» la implementa.
¡Bip, bip!2. Datos. Una interfaz IPrintable con un método PrintDetails(). Impleméntala en Person, y haz que Student (una clase hija de Person) la sobrescriba y añada la nota. Pon los dos en un array IPrintable[].
Nombre: Jorge, edad: 45Nombre: Elena, edad: 19Nota media: 5.53. Baile. Una interfaz IDanceable con un método Dance(). DanceMove (un nombre y una duración) la implementa.
Bailo salsa durante 30 segundos4. DJ. Una clase Song con un título. Una interfaz IPlayable con un método Play(). DJ la implementa y tiene un método PlaySong(Song song) que recuerda la canción y la reproduce. Si todavía no hay canción, Play() lo dice.
No hay ninguna canción elegidaReproduzco 'Bohemian Rhapsody'5. El estanque. Interfaces ISwimmable con Swim() y IFlyable con Fly(). Fish nada. Duck nada y vuela.
El pez nada.El pato nada.El pato vuela.Más tareas
- Descuento. Una interfaz IDiscountable con un método que devuelve el precio después del descuento. Impleméntala en
Product(10 % de descuento) y enSubscription(25 % de descuento para estudiantes). Calcula el total de un carrito con un producto de 50 € y una suscripción de estudiante de 12 €: 45 € + 9 € = 54 €. - CSV para todos. Haz que
Employeetambién implemente IExportable, con una línea «nombre;salario mensual». Pon empleados y facturas en un solo array IExportable[].
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Encapsulamiento · Herencia · Polimorfismo · Clases abstractas
- Continúa en: Comprobaciones de tipo · Principios de la POO · Lanzar excepciones y crear las tuyas · Listas
- Ejercicios sobre el tema: Herencia y polimorfismo