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("otchet.txt"))
{
Console.WriteLine("Пишем в файл...");
// файл автоматически "закроется" после using
}
Вывод:
Открыт файл: otchet.txt
Пишем в файл...
Закрыт файл: otchet.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. Интерфейсы для тестирования (Mock-объекты)
В тестах очень важно изолировать код от внешних зависимостей: не лезть в настоящую базу, не трогать реальные файлы. Интерфейсы позволяют использовать заглушки (mock-объекты) и тестировать код, не затрагивая реальные базы или файлы.
public class FakeDataSaver : IDataSaver
{
public bool WasCalled { get; private set; } = false;
public void Save(string reportData)
{
WasCalled = true;
Console.WriteLine("Сохранили данные в mock-объекте!");
}
}
// В тесте
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;
}
Смысл: любой фреймворк, поддерживающий биндинг данных, ждёт, что ваш класс реализует этот интерфейс — и тогда UI будет автоматически обновляться при изменении свойств.
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();
}
}
}
Плагины могут разрабатываться третьими лицами — главное, чтобы они реализовывали интерфейс. Ваше приложение становится расширяемым!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ