1. Що таке інтерфейс?
Якщо абстрактний клас — це умовний «напівфабрикат» із частковою реалізацією, то інтерфейс — це просто список вимог: що має уміти та чи інша істота (клас), щоб її можна було використовувати в певному абстрактному контексті.
У програмуванні інтерфейс — це «контракт» або «список вимог» до поведінки об’єкта. Він описує набір публічних методів, властивостей, індексаторів і подій, які клас, що реалізує цей інтерфейс, зобов’язаний надати. Інтерфейс каже: «Якщо ви хочете називатися, наприклад, IDriveable (здатним їздити), то, будь ласка, надайте методи Drive() і Stop()».
Можна уявити інтерфейс як список вимог до працівника: наприклад, якщо ви хочете взяти на роботу «кухарів», то в інтерфейсі буде написано: «Має вміти готувати, подавати страви, мити руки». Як саме кухар це робитиме — його особиста справа. Головне — щоб зовні він працював за контрактом.
Важливо: інтерфейс описує лише що клас має надати, а не як це робиться.
Коротко в термінах ООП
- Інтерфейс описує зовнішній «вигляд» (API) об’єкта: які дії він дозволяє виконувати.
- Не містить стану (жодних полів). У класичному розумінні інтерфейси теж не містять реалізації, але в сучасних версіях C# з’явилися винятки (наприклад, методи за замовчуванням), про які ми поговоримо пізніше.
- Один клас може реалізовувати скільки завгодно інтерфейсів (на відміну від наслідування класів!).
Аналогія з життя
USB-порт — кожен знає, що можна під’єднати мишу, клавіатуру, флешку, навушники, навіть кавоварку (так, трапляється й таке!). Що всередині «миші» — неважливо, аби був роз’єм USB і пристрій підтримував належний протокол. Ось це — інтерфейс!
Навіщо потрібні інтерфейси?
- Послаблюють зв’язність. Код працює з інтерфейсом, а не з конкретним класом. Програмуємо «на рівні інтерфейсу».
- Дозволяють реалізувати множинне наслідування поведінки: клас може реалізовувати кілька інтерфейсів.
- Стандартизація: можна створювати універсальні механізми: наприклад, усі об’єкти, які можна порівнювати, реалізують інтерфейс IComparable.
- Тестованість: легко підмінити конкретну реалізацію «заглушкою» на тестах.
- Підтримка плагінів: можна додавати нові «модулі» без змін наявного коду.
2. Синтаксис оголошення інтерфейсу
Переходимо до коду! У C# інтерфейс оголошується за допомогою ключового слова interface, а імена інтерфейсів прийнято починати з I:
// Оголошення інтерфейсу
public interface IPrintable
{
// Абстрактний метод — контракт
void Print();
// Можна також оголошувати властивості
string Name { get; set; }
}
Запам’ятайте: методи і властивості інтерфейсу НЕ містять реалізації (немає тіла методу, лише сигнатура — так само, як і в абстрактних методах).
Ключові особливості інтерфейсу
- Не можна оголошувати поля (змінні-члени) всередині інтерфейсу.
- Усі методи, властивості, події та індексатори за замовчуванням — public (і такими мають бути в реалізації).
- Інтерфейс не може містити конструкторів (бо він не має стану).
- Інтерфейс не залежить від того, чи є клас, що його реалізує, абстрактним чи конкретним.
Інтерфейс у дії
Додаймо інтерфейс до нашого навчального застосунку. Нехай у нас є інтерфейс «друковане» (IPrintable), який реалізує клас Report і, наприклад, новий клас Invoice.
public interface IPrintable
{
void Print();
string Name { get; set; }
}
Тепер визначимо клас, що реалізує цей інтерфейс:
public class Report : IPrintable
{
public string Name { get; set; }
public Report(string name)
{
Name = name;
}
// Реалізація методу з інтерфейсу
public void Print()
{
Console.WriteLine($"Друк звіту: {Name}");
}
}
А тепер — інший клас, але з тим самим інтерфейсом:
public class Invoice : IPrintable
{
public string Name { get; set; }
public Invoice(string name)
{
Name = name;
}
public void Print()
{
Console.WriteLine($"Друк рахунку: {Name}");
}
}
Тепер ми можемо написати метод, який працює з будь-якою «друкованою» сутністю:
public static void PrintAnything(IPrintable printable)
{
printable.Print(); // Усе! Неважливо, звіт це чи рахунок — головне, що вміє друкувати.
}
А ось і приклад використання:
var report = new Report("Щомісячний звіт");
var invoice = new Invoice("Рахунок № 12345");
PrintAnything(report); // Друк звіту: Щомісячний звіт
PrintAnything(invoice); // Друк рахунку: Рахунок № 12345
Ось так інтерфейси дозволяють писати універсальний, розширюваний і виразний код.
3. Реалізація інтерфейсів
Реалізація інтерфейсу в класі
Інтерфейс реалізується класом за допомогою двокрапки (так-так, як під час наслідування):
public class Ticket : IPrintable
{
public string Name { get; set; }
public void Print()
{
Console.WriteLine($"Друк квитка: {Name}");
}
}
Важливо: клас зобов’язаний реалізувати всі члени інтерфейсу. При цьому реалізовані члени мають бути public.
Якщо не реалізувати хоча б один член:
public class BrokenTicket : IPrintable
{
// Пропущена реалізація Print()
public string Name { get; set; }
}
// Помилка компіляції: 'BrokenTicket' не реалізує член інтерфейсу 'IPrintable.Print()'
Кілька інтерфейсів
Клас може реалізовувати кілька інтерфейсів через кому:
public interface IStorable
{
void Store();
}
public class MultiPurposeDoc : IPrintable, IStorable
{
public string Name { get; set; }
public void Print()
{
Console.WriteLine("Друк документа");
}
public void Store()
{
Console.WriteLine("Збереження документа");
}
}
4. Навіщо потрібні інтерфейси?
Можливо, зараз у вас крутиться думка: «Ну добре, синтаксис зрозумілий. А навіщо це все потрібно в реальному житті, окрім як щоб ускладнити мені життя на цьому курсі?» Мушу вас розчарувати: інтерфейси — один із найчастіше використовуваних інструментів у професійній розробці.
Розділення відповідальності та слабка зв’язність (Decoupling / Loose Coupling):
- Уявіть, що ви розробляєте плеєр для відтворення музики. Йому неважливо, звідки береться музика — з локального файлу, з інтернету чи з CD-диска. Важливо лише, щоб джерело музики могло надати аудіопотік.
- Ви можете визначити інтерфейс IAudioSource з методом GetAudioStream().
- Тоді у вас будуть класи FileAudioSource, InternetAudioSource, CDAudioSource, які реалізують цей інтерфейс.
- Ваш плеєр працюватиме з IAudioSource, не знаючи конкретного типу. Якщо завтра з’явиться новий тип джерела, наприклад, BluetoothAudioSource, вам не доведеться змінювати код плеєра! Просто створіть новий клас, що реалізує IAudioSource. Це робить вашу систему набагато гнучкішою і легко розширюваною. Це слабка зв’язність — компоненти залежать від абстракцій (інтерфейсів), а не від конкретних реалізацій.
Поліморфізм та уніфікована обробка:
Як ми бачили у прикладі з PrintAnything, ви можете мати набір об’єктів різних типів, об’єднаних спільною поведінкою, описаною в інтерфейсі. Ви викликаєте один і той самий метод (Print()) у всіх таких об’єктів, не знаючи, хто саме перед вами — звіт, рахунок чи квиток. Це дозволяє писати дуже лаконічний і універсальний код.
Модульне тестування (unit testing):
Це, мабуть, одне з найважливіших застосувань інтерфейсів. Коли ви тестуєте певний компонент своєї системи, йому часто потрібні інші компоненти для роботи (наприклад, клас, що зберігає дані, може залежати від класу, який працює з базою даних).
Замість того щоб передавати справжній клас DatabaseSaver (який вимагає реальної бази даних для тестування!), ви можете передати підмінний (mock) об’єкт, який просто реалізує інтерфейс IDataSaver. Такий mock-об’єкт лише імітуватиме збереження, не звертаючись до реальної бази. Це дозволяє тестувати компоненти ізольовано, швидко й без зовнішніх залежностей.
Розробка API та фреймворків:
Коли ви створюєте бібліотеку або фреймворк, хочете надати розробникам «точки розширення». Інтерфейси ідеально підходять для цього. Ви можете сказати: «Якщо хочете, щоб ваш компонент працював із моєю системою, реалізуйте ось цей інтерфейс». Стандартні бібліотеки .NET містять багато інтерфейсів (наприклад, IEnumerable<T>, IDisposable, IComparable<T>) — вони визначають контракти для найпоширеніших сценаріїв.
Програмування на рівні інтерфейсів (Programming to an Interface):
Досвідчені розробники часто радять: «Програмуйте на рівні інтерфейсів, а не реалізацій». Це означає, що коли ви визначаєте тип змінної або параметра методу, замість конкретного класу (Car) краще використовувати інтерфейс (IDriveable). Це робить ваш код гнучкішим і менш залежним від деталей реалізації, дозволяючи легко замінювати одну реалізацію іншою.
5. Типові помилки при роботі з інтерфейсами
Помилка № 1: спроба створити примірник інтерфейсу.
Ви можете написати Cat murzik = new Cat("Мурзик", 3);, бо Cat — це конкретний клас. Але ви не можете написати ITalkable talker = new ITalkable();. Інтерфейс — це лише контракт, шаблон. Він не містить реалізації й не може бути створений безпосередньо. Це як креслення, а не готовий будинок.
Помилка № 2: забута реалізація всіх членів інтерфейсу.
Якщо ви вказали, що ваш клас реалізує інтерфейс, наприклад IMyInterface, то він зобов’язаний реалізувати всі його методи. Навіть один пропущений метод спричинить помилку компіляції: MyClass не реалізує IMyInterface.TheMissingMethod().
Помилка № 3: неправильні модифікатори доступу під час реалізації.
Методи інтерфейсу неявно public, і при реалізації вони теж мають бути public. Якщо спробувати зробити метод private або protected, компілятор видасть помилку. Обіцяли — реалізуйте відкрито.
Помилка № 4: спроба додати поля або конструктори до інтерфейсу.
Інтерфейси описують поведінку, а не стан. Тому не можна додавати до них поля або конструктори. Якщо спробуєте — отримаєте помилку компіляції. Дозволені лише властивості, і то — як опис гетерів/сетерів.
Помилка № 5: плутанина між override і реалізацією інтерфейсу.
Ключове слово override використовують для перевизначення методів базового класу. Але під час реалізації інтерфейсу воно не потрібне — просто оголошуйте метод із модифікатором public і потрібною сигнатурою. Це важливий нюанс, який легко пропустити.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ