JavaRush /Курси /C# SELF /Реалізація кількох інтерфейсів

Реалізація кількох інтерфейсів

C# SELF
Рівень 23 , Лекція 2
Відкрита

1. Навіщо реалізовувати кілька інтерфейсів?

Коли ви проєктуєте реальну систему, об’єкти часто виконують не одну «роль», а одразу кілька. Уявімо: у вас є електронна книга, яку можна не лише читати, а й редагувати та навіть зберігати у хмарі. У термінах об’єктно-орієнтованого програмування (ООП) це означає, що об’єкт має реалізувати одразу кілька інтерфейсів:

  • IReadable — читання контенту.
  • IWritable — редагування контенту.
  • ISyncable — синхронізація з віддаленим сховищем.

Ось тут і стає у пригоді можливість реалізувати кілька інтерфейсів. Це — суперсила C# (і ООП загалом), якої класи не мають: ви не можете успадковувати від двох і більше класів, зате інтерфейсів можна реалізувати скільки завгодно.

Аналогія з реального життя

Уявімо співробітника в компанії. Василь може бути одночасно:

  • Програмістом (пише код).
  • Тестувальником (іноді перевіряє чужий код).
  • Менеджером (планує спринти або денну норму кави).

Ці «ролі» мають цілком різні обов’язки. Але Василь спокійно справляється з кожним із контрактів! У програмуванні — те саме: клас реалізує інтерфейси та бере на себе їхні «обов’язки».

2. Синтаксис реалізації кількох інтерфейсів

Усе дуже просто: під час оголошення класу перелічуєте їх через кому:


public interface IReadable
{
    void Read();
}

public interface IWritable
{
    void Write(string text);
}

public class Note : IReadable, IWritable
{
    private string content = "";

    public void Read()
    {
        Console.WriteLine("Записка: " + content);
    }

    public void Write(string text)
    {
        content = text;
        Console.WriteLine("Записка оновлена!");
    }
}

У цьому прикладі клас Note реалізує обидва інтерфейси. Отже, він обов’язково має реалізувати обидва методи: і Read, і Write.

3. Як це виглядає на прикладі «нашого» застосунку

У попередніх прикладах ми розвивали простий банківський застосунок для роботи з рахунками. Тепер уявімо: нам потрібен універсальний клас для документа, який можна надрукувати (IPrintable), зберегти у файл (ISavable) і, можливо, надіслати електронною поштою (IEmailable).

Визначимо інтерфейси:


public interface IPrintable
{
    void Print();
}

public interface ISavable
{
    void Save(string filePath);
}

public interface IEmailable
{
    void Email(string toAddress);
}

Клас, що реалізує усе й одразу:


public class Statement : IPrintable, ISavable, IEmailable
{
    public string Content { get; set; }

    public void Print()
    {
        Console.WriteLine("Друкую виписку...");
        Console.WriteLine(Content);
    }

    public void Save(string filePath)
    {
        // Використовуємо File.WriteAllText зі стандартної бібліотеки.
        File.WriteAllText(filePath, Content);
        Console.WriteLine($"Виписка збережена у файл: {filePath}");
    }

    public void Email(string toAddress)
    {
        Console.WriteLine($"Виписку надіслано на електронну пошту: {toAddress} (симуляція)");
    }
}

Тепер цей клас можна використовувати у коді як завгодно:


var stat = new Statement { Content = "Операції за місяць: +1000 од., -500 кд." };
stat.Print();
stat.Save("statement.txt");
stat.Email("boss@bank.corp");

До речі, на практиці дуже зручно передавати такий об’єкт у методи, які вимагають конкретні інтерфейси, не переймаючись про його інші можливості. Наприклад, метод для друку може приймати параметр IPrintable і не знати, що всередині — і «зберігач», і «поштовик».

4. Застосування інтерфейсних посилань: де «видно», що ваш об’єкт уміє?

Ось тут починається магія. Коли ви реалізували кілька інтерфейсів, можна звертатися до об’єкта лише як до одного з його контрактів. Наприклад:


IPrintable printable = new Statement { Content = "Лекція з абстракції" };
printable.Print(); // Можна тільки друкувати

// printable.Save("file.txt"); // Помилка: інтерфейс IPrintable нічого не знає про Save.

Але якщо перемкнутися на інший інтерфейс:


ISavable savable = printable as ISavable;
if (savable != null)
{
    savable.Save("file.txt");
}

Це зручно, коли ви передаєте об’єкт у метод, який приймає посилання на певний інтерфейс. Такий підхід знижує зв’язаність, і код стає дуже гнучким: ви можете додавати нові реалізації, не змінюючи старий код.

5. Множинна реалізація і однакові методи в різних інтерфейсах

А ось тут починається цікаве! Що, якщо два інтерфейси вимагають метод з однаковим ім’ям, але різним змістом? Наприклад, уявімо інтерфейси для кавомашини:


public interface IStartable
{
    void Start();
}

public interface IRunnable
{
    void Start();
}

Кавомашина може бути як «запусканою» (IStartable — стартує процес приготування), так і «runnable» (IRunnable — починає працювати загалом).

Реалізація за замовчуванням (звичайна):


public class CoffeeMachine : IStartable, IRunnable
{
    public void Start()
    {
        Console.WriteLine("Кавомашина стартує для обох ролей!");
    }
}

У цьому випадку одна реалізація Start() задовольняє обидва інтерфейси.

Що, якщо потрібна різна поведінка?

Можна використати явну реалізацію інтерфейсу:


public class CoffeeMachine : IStartable, IRunnable
{
    void IStartable.Start()
    {
        Console.WriteLine("Старт: приготування напою розпочато!");
    }

    void IRunnable.Start()
    {
        Console.WriteLine("Старт: машину переведено у робочий режим.");
    }
}

У цьому випадку викликати конкретну реалізацію можна лише через інтерфейсну змінну:


CoffeeMachine cm = new CoffeeMachine();

IStartable startable = cm;
startable.Start(); // Старт: приготування напою розпочато!

IRunnable runnable = cm;
runnable.Start(); // Старт: машину переведено у робочий режим.

// cm.Start(); // Не скомпілюється! Старт недоступний як метод самого класу.

До речі, такий прийом часто використовують у стандартній бібліотеці .NET — наприклад, коли клас реалізує одразу кілька схожих інтерфейсів із різних фреймворків, і кожен вимагає власної поведінки.

6. Відмінність реалізації кількох інтерфейсів і наслідування

Можливість Клас (наслідування) Інтерфейси
Кількість базових типів Тільки один Скільки завгодно
Наслідування коду Так (можна мати базову реалізацію) Ні, тільки сигнатури (окрім методів за замовчуванням)
Зберігання стану Так Ні
Додавання нової ролі Ні (або складно: через композицію) Легко — реалізувати інтерфейс
Найкраще для… «Фізичні» ієрархії «Ролі»/логічні можливості

7. Практичне застосування: «гібридні» об’єкти

Завдяки множинній реалізації інтерфейсів ви можете створювати класи з унікальним набором «ролей», не вдаючись до зайвого наслідування.

Наприклад, у нашому застосунку для банку можна створити клас, який уміє і зберігати інформацію про себе, і перевіряти свою валідність, і друкувати — усе через різні інтерфейси:


public interface IValidatable
{
    bool Validate();
}

public class Check : IPrintable, ISavable, IValidatable
{
    public string Data { get; set; }

    public void Print()
    {
        Console.WriteLine("Друк чека: " + Data);
    }

    public void Save(string filePath)
    {
        File.WriteAllText(filePath, Data);
        Console.WriteLine("Чек збережено: " + filePath);
    }

    public bool Validate()
    {
        return !string.IsNullOrEmpty(Data);
    }
}

Такий підхід дозволяє будувати легко розширювані архітектури, де класи комбінують ролі за потреби. Якщо треба додати новий «обов’язок» — просто реалізуйте новий інтерфейс.

8. Типові помилки та підводні камені

Поширена помилка новачків: забути реалізувати всі члени інтерфейсу. Компілятор цього не пробачить — згенерує помилку й підкаже, чого бракує. Типове формулювання: «Клас має реалізувати інтерфейсний член».

Ще одна поширена проблема — плутанина з доступністю методів. Якщо ви реалізували метод явно, то він недоступний через змінну самого класу — тільки через інтерфейс.

Також типова ситуація — рефакторинг: ви вносите зміни в інтерфейс (наприклад, додаєте метод), але не оновили всі реалізації. Це призводить до помилок під час компіляції. Тому під час проєктування намагайтеся не змінювати інтерфейси після того, як багато класів уже їх використовують.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ