Към съдържанието
wannacode

Интерфейси

Проблем

Заплати и фактури

В края на месеца фирмата плаща две неща: заплатите на служителите и фактурите на доставчиците. Искаш един масив и един цикъл: на кого, колко, и накрая общата сума.

Масив Employee[] не става: фактурата не е служител. Да я накараш да наследи Employee е грешно. Изречението „фактурата е служител“ не е вярно, а тя ще получи и име, и заплата, и повишение.

Общ родител Payment също не става. Manager вече има родител, Employee, а класът може да има само един. Служителите и фактурите нямат общо нищо, освен че могат да бъдат платени.

Идеята

Интерфейс: договор „мога да“

Интерфейсът е списък с методи без тяло. Той казва какво трябва да може класът, но не и как:

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("Доставчик на ток", 340);
Console.WriteLine(power.Payee() + ": " + power.AmountDue() + " €");
Изход
Доставчик на ток: 340 €

Интерфейсът се реализира със същото двоеточие като наследяването. Методите се пишат без override: няма какво да се презаписва, интерфейсът няма свои версии.

В C# имената на интерфейсите започват с I: IPayable, IComparable, IDisposable. Така от class Invoice : IPayable се вижда веднага, че двоеточието е за интерфейс.

Методите на интерфейса са винаги public. Те са обещание към всички, затова и в класа трябва да са public.

Виж го

Един цикъл за всичко, което се плаща

Служителят също приема договора. Неговата сума е месечната заплата:

class Employee : IPayable
{
// ... Name, Salary, конструкторът, MonthlyPay()
public int AmountDue()
{
return MonthlyPay();
}
public string Payee()
{
return Name;
}
}

Сега и служителите, и фактурите са IPayable. Интерфейсът е тип: може да е тип на променлива, на параметър, на масив.

IPayable[] payments =
{
new Employee("Иван", 2000),
new Manager("Мария", 3000, 500),
new Invoice("Доставчик на ток", 340),
new Invoice("Наемодател", 1800)
};
int total = 0;
foreach (IPayable p in payments)
{
Console.WriteLine(p.Payee() + ": " + p.AmountDue() + " €");
total += p.AmountDue();
}
Console.WriteLine("Общо: " + total + " €");
Изход
Иван: 2000 €
Мария: 3500 €
Доставчик на ток: 340 €
Наемодател: 1800 €
Общо: 7640 €

Мениджърът не пише нищо ново. Той наследява от Employee и договора, и AmountDue(). А AmountDue() вика MonthlyPay(), който мениджърът е презаписал. Затова Мария получава 3500 €.

IPayable[] payments =
{
new Employee("Иван", 2000),
new Manager("Мария", 3000, 500),
new Invoice("Доставчик на ток", 340)
};
int total = 0;
foreach (IPayable p in payments)
{
total += p.AmountDue();
}
Console.WriteLine("Общо: " + total + " €");
Стъпка по стъпка, с думи
  1. Масивът е от тип IPayable. Стрелките сочат три класа, но стигат само до договора им.
  2. total започва от 0.
  3. p сочи служителя: AmountDue() връща заплатата, 2000.
  4. Мениджърът е служител, затова и той е платим. Заплата плюс бонус: 3500.
  5. Фактурата не е служител, но изпълнява същия договор: 340.
  6. Цикълът не знае класовете. Знае само, че всеки може да бъде платен.

Пробвай

Каква е сумата?

Какво ще отпечата програмата? Класовете са от горната карта.

IPayable p = new Manager("Мария", 3000, 500);
Console.WriteLine(p.AmountDue());
Отговор

Идеята

Един родител, много договори

Счетоводството иска фактурите и в CSV формат: по един ред „доставчик;сума“. Това е втори договор. Класът може да реализира колкото интерфейса иска, разделени със запетая:

interface IExportable
{
string ToCsv();
}
class Invoice : IPayable, IExportable
{
// ... както преди
public string ToCsv()
{
return Supplier + ";" + Amount;
}
}
IExportable[] forAccounting = { new Invoice("Доставчик на ток", 340), new Invoice("Наемодател", 1800) };
foreach (IExportable e in forAccounting)
{
Console.WriteLine(e.ToCsv());
}
Изход
Доставчик на ток;340
Наемодател;1800

Ако класът има и родител, и интерфейси, родителят е пръв:

class Manager : Employee, IExportable

Първото име след двоеточието може да е клас. Всички останали са интерфейси.

Родителят е един, договорите са много. Така клас с вече зает родител пак може да влезе в общ масив с напълно различни класове.

Пробвай

Намери грешката

Тази фактура не се компилира. На кой ред е грешката?

Избери реда с грешката.

Пробвай

Кое липсва?

class Invoice  IPayable
{
    // ...
}

Идеята

Абстрактен клас или интерфейс?

И двете са родители, от които не се правят обекти, и двете задължават наследниците. Разликата е в това какво казват:

Абстрактен клас Интерфейс
Казва „X е Y“ „X може да“
Колко на клас един колкото искаш
Полета и конструктор да не
Общ код да почти не
  • Абстрактен клас, когато наследниците са един вид и имат общ код и данни: фигурите имат име и описание, служителите имат име и заплата.
  • Интерфейс, когато класовете са различни, а им трябва обща способност: служител и фактура „могат да бъдат платени“, птица и самолет „могат да летят“.
Често се ползват заедно: Employee е родител на мениджъра и стажанта и същевременно реализира IPayable.
По-точно, за любопитните
  • От C# 8 интерфейсът може да има метод по подразбиране, с тяло. Класовете го получават наготово, а могат и да напишат свой. Така в интерфейс се добавя нов метод, без да се чупят класовете, които вече го реализират. Такъв метод се вика през променлива от типа на интерфейса (IFlyable f), а не през Bird:
interface IFlyable
{
void Fly();
void Glide()
{
Console.WriteLine("Плъзга се във въздуха");
}
}
class Bird : IFlyable
{
public void Fly()
{
Console.WriteLine("Птицата маха с крила");
}
}
class Plane : IFlyable
{
public void Fly()
{
Console.WriteLine("Самолетът излита");
}
}
IFlyable[] flyers = { new Bird(), new Plane() };
foreach (IFlyable f in flyers)
{
f.Fly();
f.Glide();
}
Изход
Птицата маха с крила
Плъзга се във въздуха
Самолетът излита
Плъзга се във въздуха
  • Интерфейсът не може да има полета. Може да има свойства, но само като обещание: int Amount { get; } казва „всеки клас ще има такова свойство“.

Упражнение

Договори

Реши задачите в редактора си.

1. Клаксон. Интерфейс IHonkable с метод Honk(). SimpleCar от модул „Класове“ го реализира.

Изход
Бип-бип!

2. Подробности. Интерфейс IPrintable с метод PrintDetails(). Реализирай го в Person, а Student (наследник на Person) да го презапише и да добави успеха. Сложи двамата в масив IPrintable[].

Изход
Име: Георги, възраст: 45
Име: Елена, възраст: 19
Среден успех: 5.5

3. Танц. Интерфейс IDanceable с метод Dance(). DanceMove (име и продължителност) го реализира.

Изход
Танцувам салса 30 секунди

4. Диджей. Клас Song със заглавие. Интерфейс IPlayable с метод Play(). DJ го реализира и има метод PlaySong(Song song), който запомня песента и я пуска. Ако още няма песен, Play() казва това.

Изход
Няма избрана песен
Пускам 'Bohemian Rhapsody'

5. Езерото. Интерфейси ISwimmable с Swim() и IFlyable с Fly(). Fish плува. Duck и плува, и лети.

Изход
Рибата плува.
Патицата плува.
Патицата лети.

Още задачи

  • Отстъпка. Интерфейс IDiscountable с метод, който връща цената след отстъпка. Реализирай го в Product (10% отстъпка) и в Subscription (25% отстъпка за студенти). Сметни сумата на количка с продукт за 50 € и студентски абонамент за 12 €: 45 + 9 = 54 €.
  • CSV за всички. Накарай и Employee да реализира IExportable, с ред „име;месечна заплата“. Сложи служители и фактури в един масив IExportable[].

Тест

Провери се

1. Колко интерфейса може да реализира един клас?
2. Компилира ли се new IPayable()?
3. Класът реализира метод на интерфейса без public. Какво става?
4. Пише ли се override пред метод, който реализира интерфейс?
5. Служители и фактури трябва да се плащат в един цикъл. Те нямат общ код. Какво е по-подходящо?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·