1. Інтерфейси в бізнес-логіці: кнопки та дії
Це фінальна лекція про інтерфейси, тож тут буде багато практичних прикладів, аби ви краще зрозуміли, як інтерфейси використовують на практиці.
Інтерфейси особливо цінні у великих застосунках. Тому нижче наведено складніші випадки, іноді з випередженням програми курсу. Якщо цікаво — чудово! Якщо ні — перейдіть на наступний рівень :P
Найпростіший приклад — кнопки та клацання на них
Продовжимо розвивати наш навчальний консольний застосунок — уявімо, що маємо просту систему користувацьких елементів (наприклад, кнопки, текстові поля та перемикачі) в консольному меню.
Ми хочемо, щоб одні елементи реагували на натискання (кнопки), а інші — ні (наприклад, статичний заголовок). Інтерфейси дозволяють елегантно реалізувати такі сценарії.
Крок 1: Створюємо інтерфейс
// Компонент, який можна клацнути:
public interface IClickable
{
void Click();
}
Крок 2: Застосовуємо інтерфейс для різних типів елементів
// Базовий клас для всіх елементів меню
public class MenuItem
{
public string Title { get; set; }
public MenuItem(string title)
{
Title = title;
}
public virtual void Display()
{
Console.WriteLine(Title);
}
}
// Кнопка: її можна клацнути
public class Button : MenuItem, IClickable
{
public Button(string title) : base(title) {}
public void Click()
{
Console.WriteLine($"[Кнопка] {Title} була натиснута!");
}
}
// Просто напис: клацати не можна
public class Label : MenuItem
{
public Label(string title) : base(title) {}
}
Крок 3: Використовуємо поліморфізм інтерфейсу
IClickable[] clickableItems = new IClickable[]
{
new Button("Зберегти"),
new Button("Вийти")
// Label тут додати не можна — вона не IClickable!
};
foreach (var item in clickableItems)
{
item.Click();
}
Бачите «магію»? Список приймає лише тих, хто «вміє» Click, тож ви не отримаєте помилки під час виконання, якщо спробуєте клацнути на невідповідному обʼєкті.
2. Інтерфейси та патерн «Стратегія»: вибір алгоритму на льоту
Уявіть: ви пишете застосунок, у якому можна зберігати звіти різними способами: у файл, у базу даних, у «хмару», а може, і в Telegram вашому керівникові (не питайте, всяке буває).
Крок 1: Формулюємо завдання
Потрібно, щоб клас ReportGenerator міг працювати з будь-яким способом збереження, не знаючи деталей реалізації.
Крок 2: Описуємо інтерфейс-стратегію
public interface IDataSaver
{
void Save(string reportData);
}
Крок 3: Реалізуємо різні варіанти
public class FileDataSaver : IDataSaver
{
public void Save(string reportData)
{
Console.WriteLine("[FileDataSaver] Зберігаю у файл...\n" + reportData);
// Тут міг би бути код для File.WriteAllText(...)
}
}
public class DatabaseDataSaver : IDataSaver
{
public void Save(string reportData)
{
Console.WriteLine("[DatabaseDataSaver] Зберігаю у базу даних...\n" + reportData);
// Тут міг би бути код для запису у базу даних
}
}
Крок 4: Використовуємо інтерфейс для підміни стратегії збереження
public class ReportGenerator
{
private readonly IDataSaver _dataSaver;
public ReportGenerator(IDataSaver dataSaver)
{
_dataSaver = dataSaver;
}
public void GenerateReport()
{
string report = "Це важливий звіт!";
Console.WriteLine("Генеруємо звіт...");
_dataSaver.Save(report);
}
}
Демонстрація гнучкості
// Можна підмінити спосіб збереження без переписування ReportGenerator!
IDataSaver fileSaver = new FileDataSaver();
ReportGenerator fileReport = new ReportGenerator(fileSaver);
fileReport.GenerateReport();
IDataSaver dbSaver = new DatabaseDataSaver();
ReportGenerator dbReport = new ReportGenerator(dbSaver);
dbReport.GenerateReport();
Практичний сенс: якщо завтра ви захочете писати звіти в новий супермодний сервіс, достатньо створити новий клас, що реалізує інтерфейс, і підставити обʼєкт у свою архітектуру, не змінюючи жодного рядка наявного коду.
3. Інтерфейси в .NET: IDisposable і using
Один із найпоширеніших інтерфейсів у .NET — IDisposable. Його реалізують усі класи, які працюють з некерованими ресурсами: файлами, потоками, мережевими з’єднаннями.
Навіщо потрібен IDisposable?
Коли ви працюєте з ресурсом, який обов’язково треба звільнити (наприклад, закрити файл), ви реалізуєте IDisposable і пишете метод Dispose. Це дозволяє передавати такі об’єкти в оператор using, який гарантує виклик Dispose під час виходу з блоку.
Приклад: симуляція роботи з файлом
public class FakeFile : IDisposable
{
public string FileName { get; }
public FakeFile(string fileName)
{
FileName = fileName;
Console.WriteLine($"Відкрито файл: {fileName}");
}
public void Dispose()
{
Console.WriteLine($"Закрито файл: {FileName}");
}
}
// В основній програмі:
using (var file = new FakeFile("zvit.txt"))
{
Console.WriteLine("Пишемо у файл...");
// файл автоматично "закриється" після using
}
Виведення:
Відкрито файл: zvit.txt
Пишемо у файл...
Закрито файл: zvit.txt
Справді корисно: ви не забудете закрити файл — інтерфейс і інфраструктура мови вас захистять.
4. Інтерфейси в колекціях та LINQ
Коли ви працюєте зі списками, масивами, словниками — ви вже використовуєте інтерфейси, навіть якщо про це не замислюєтеся.
List<int> list = new List<int> { 1, 2, 3 };
IEnumerable<int> enumerable = list; // усе гаразд!
// Тепер ви можете перебрати елементи так:
foreach(var x in enumerable)
{
Console.WriteLine(x);
}
Більшість методів LINQ працюють з колекціями через інтерфейс IEnumerable<T>. Це дозволяє писати код, який не залежить від конкретного типу колекції.
Навіщо це потрібно?
Ви можете замінити List<T> на T[], HashSet<T> або навіть власну колекцію — і ваш код працюватиме далі!
5. Інтерфейси для тестування (мок-обʼєкти)
У тестах дуже важливо ізолювати код від зовнішніх залежностей: не звертатися до справжньої бази, не чіпати реальні файли. Інтерфейси дозволяють використовувати заглушки (мок-обʼєкти) й тестувати код, не торкаючись реальних баз чи файлів.
public class FakeDataSaver : IDataSaver
{
public bool WasCalled { get; private set; } = false;
public void Save(string reportData)
{
WasCalled = true;
Console.WriteLine("Зберегли дані у мок-обʼєкті!");
}
}
// У тесті
FakeDataSaver saver = new FakeDataSaver();
ReportGenerator generator = new ReportGenerator(saver);
generator.GenerateReport();
Console.WriteLine($"Чи було викликано Save? {saver.WasCalled}");
Результат: тест не залежить від реального світу, але підтверджує, що потрібний метод було викликано!
6. Явна реалізація інтерфейсів для розвʼязання конфліктів
Іноді клас має реалізувати два інтерфейси з однаковими методами, але логіка цих методів різниться.
public interface IFlyable
{
void Move();
}
public interface ISwimmable
{
void Move();
}
public class Duck : IFlyable, ISwimmable
{
// Явна реалізація
void IFlyable.Move()
{
Console.WriteLine("Качка летить!");
}
void ISwimmable.Move()
{
Console.WriteLine("Качка пливе!");
}
}
Duck duck = new Duck();
// duck.Move(); // Помилка: немає такого методу!
((IFlyable)duck).Move(); // "Качка летить!"
((ISwimmable)duck).Move(); // "Качка пливе!"
Це може здаватися суворим, але іноді саме цього вимагають специфікації!
7. Інтерфейси в подієвій моделі: INotifyPropertyChanged
У .NET є стандартні інтерфейси для підтримки подій — наприклад, коли в моделях даних щось змінюється, і UI має про це дізнатися (дуже часто у WPF, MAUI та інших GUI).
using System.ComponentModel;
public class Person : INotifyPropertyChanged
{
private string name;
public string Name
{
get => name;
set
{
if (name != value)
{
name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
}
public event PropertyChangedEventHandler? PropertyChanged;
}
Сенс: будь-який фреймворк, що підтримує data binding, очікує, що ваш клас реалізує цей інтерфейс — і тоді інтерфейс користувача автоматично оновлюватиметься під час зміни властивостей.
8. Інтерфейси в патерні «Фабрика» (Factory)
Інтерфейси ідеально підходять для побудови фабрик — класів, які створюють різні, але взаємозамінні обʼєкти.
public interface ITransport
{
void Move();
}
public class Bicycle : ITransport
{
public void Move() => Console.WriteLine("Їдемо на велосипеді!");
}
public class Car : ITransport
{
public void Move() => Console.WriteLine("Їдемо на машині!");
}
public class TransportFactory
{
public static ITransport Create(string type)
{
return type switch
{
"bike" => new Bicycle(),
"car" => new Car(),
_ => throw new ArgumentException("Невідомий тип транспорту")
};
}
}
ITransport transport = TransportFactory.Create("bike");
transport.Move(); // "Їдемо на велосипеді!"
9. Інтерфейси для подій: приклад власної події
Опишемо інтерфейс «слухача події»:
public interface ILoginListener
{
void OnLogin(string userName);
}
// Клас, який викликає подію
public class LoginManager
{
private List<ILoginListener> listeners = new();
public void Subscribe(ILoginListener listener) => listeners.Add(listener);
public void Login(string userName)
{
Console.WriteLine($"Користувач {userName} увійшов.");
foreach (var listener in listeners)
listener.OnLogin(userName);
}
}
// Клас-реалізатор інтерфейсу
public class WelcomeMessage : ILoginListener
{
public void OnLogin(string userName)
{
Console.WriteLine($"Вітаємо, {userName}!");
}
}
LoginManager manager = new();
manager.Subscribe(new WelcomeMessage());
manager.Login("Вася");
// Користувач Вася увійшов.
// Вітаємо, Вася!
На співбесіді: якщо запитають — «як би ви реалізували власну подієву систему?», сміливо розповідайте про інтерфейси-слухачі!
10. Інтерфейси для плагінів (розширюваність застосунків)
Багато великих застосунків підтримують плагіни. Завдяки інтерфейсам ваш застосунок може «завантажувати» нові модулі на льоту, не знаючи їхньої внутрішньої кухні.
public interface IPlugin
{
string Name { get; }
void Run();
}
// Твій застосунок:
public class PluginLoader
{
public void LoadAndRun(IEnumerable<IPlugin> plugins)
{
foreach (var plugin in plugins)
{
Console.WriteLine($"Запускаю плагін: {plugin.Name}");
plugin.Run();
}
}
}
Плагіни можуть розробляти треті особи — головне, щоб вони реалізували інтерфейс. Ваш застосунок стає розширюваним!
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ