JavaRush /Курси /C# SELF /Контракти інтерфейсу

Контракти інтерфейсу

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

1. Вступ

Отже, ми переконалися, що інтерфейси — це потужні контракти. А що ж таке програмування на рівні інтерфейсів? Це філософія проєктування, яка каже: «Залежте від абстракцій, а не від конкретних реалізацій».

Повернімося до аналогії. Ви хочете приготувати каву. Купуєте кавомашину. Вам байдуже, чи це конкретна модель «Супер-Пупер-Машина V3000» чи «Мега-Автомат Бариста 5000», чи не так? Головне, що цей пристрій уміє «робити каву», тобто реалізує «контракт» ICoffeeMaker. Ви просто натискаєте кнопку «Зробити каву» — і вона працює. А як саме — з меленої кави, капсул чи ще чогось — вас, як користувачів, особливо не хвилює.

У коді це означає, що замість того, щоб у своїх методах вимагати конкретний тип Document чи Image, ви вимагатимете IPrintable. Ваш код працюватиме з будь-яким об’єктом, який реалізує цей інтерфейс, не знаючи його конкретного типу.

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

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

Приклад


public interface IPrintable
{
    void Print();
}

Контракт: кожен, хто реалізує IPrintable, має вміти виводити себе на екран. Як — це вже деталі.

2. Контракт як точка взаємодії між частинами системи

Як це працює на практиці

Система може складатися з десятків класів, створених різними людьми, у різний час і з різною метою. Але якщо всі вони дотримуються одного контракту (тобто реалізують один інтерфейс), то їх можна використовувати однаково.

Приклад з реального застосунку: уявіть базу клієнтів. Клас Customer, клас Employee, клас Contractor. Кожен зберігає дані по-своєму, але якщо всі вони реалізують, скажімо, інтерфейс IContactInfo, який вимагає методи GetEmail() і GetPhoneNumber(), то для зовнішнього коду не має значення, до якого типу належить об’єкт — головне, що можна отримати електронну адресу і номер телефону.


public interface IContactInfo
{
    string GetEmail();
    string GetPhoneNumber();
}

public class Customer : IContactInfo
{
    public string Email { get; set; }
    public string Phone { get; set; }

    public string GetEmail() => Email;
    public string GetPhoneNumber() => Phone;
}

// Аналогічно для Employee і Contractor...

Тепер, якщо вам потрібно надрукувати дані всіх, з ким у вашій компанії є контакт (неважливо, хто це), ви просто проходитеся списком IContactInfo, викликаєте потрібні методи — і все працює.

3. Програмування «на рівні інтерфейсів»

Програмувати на рівні інтерфейсів — означає писати код, який залежить не від конкретних класів, а лише від інтерфейсів (тобто контрактів). Клас, який реалізує інтерфейс, може бути будь-яким, доки він виконує вимоги інтерфейсу.

Чому це важливо?

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

Приклад — знайома програма: «Банківський рахунок»

У попередніх прикладах застосунку в нас був абстрактний клас BankAccount з абстрактним методом Withdraw(), а конкретні типи (SavingsAccount, CheckingAccount) реалізовували деталі.

Додаймо тепер інтерфейс — наприклад, для виведення інформації про баланс:


public interface IBalanceReporter
{
    void ReportBalance();
}

public abstract class BankAccount : IBalanceReporter
{
    public double Balance { get; set; }

    public abstract void Withdraw(double amount);

    public void ReportBalance()
    {
        Console.WriteLine($"Поточний баланс: {Balance} євро");
    }
}

Тепер усі, хто працює з IBalanceReporter, можуть викликати ReportBalance() незалежно від конкретного типу рахунку.

4. Загальна обробка і поліморфізм через контракт

Приклад: загальний обробник

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


static void PrintAllBalances(IBalanceReporter[] accounts)
{
    foreach (var reporter in accounts)
    {
        reporter.ReportBalance();
    }
}

До цього списку можуть входити будь-які об’єкти, що реалізують IBalanceReporter: SavingsAccount, CheckingAccount, навіть якийсь MockAccountForTesting. І жодної магії — усе чесно працює.

Схематично: що дає контракт інтерфейсу


+-------------------+      реалізує     +-------------------+
|   BankAccount     |  <---------------- |  IBalanceReporter |
|  (SavingsAccount) |                    |-------------------|
|  (CheckingAccount)|                    | + ReportBalance() |
+-------------------+                    +-------------------+
         |                                   ^
         |                                   |
         +-----------+-----------------------+
                     |
        Будь-який інший клас, що реалізує контракт
Схема: контракт інтерфейсу як точка взаємодії

5. Контракти, бізнес-логіка і написання архітектури

Контракт інтерфейсу чітко відділяє те, що має вміти клас, від того, як він це реалізує. Тому архітектори ПЗ люблять проєктувати логіку систем саме через інтерфейси. Часто спочатку розробляється інтерфейс (контракт), а його реалізація з’являється пізніше.

Приклад з життя: платіжні системи

Електронні гаманці, картки, PayPal, криптовалюта… У кожної — свої деталі, але якщо абстрагуватися і створити інтерфейс IPaymentProvider:


public interface IPaymentProvider
{
    void Pay(decimal amount);
    bool Refund(decimal amount);
}

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

6. Винесення бізнес-логіки в контракт

Контракт дає змогу винести основні бізнес-правила на рівень інтерфейсу, а деталі (наприклад, перевірка ліміту, нарахування кешбеку тощо) залишити на розсуд конкретних класів.

Ще один приклад


public interface ILogger
{
    void LogInfo(string message);
    void LogError(string message);
}

public class ConsoleLogger : ILogger
{
    public void LogInfo(string message)  => Console.WriteLine($"INFO: {message}");
    public void LogError(string message) => Console.WriteLine($"ERROR: {message}");
}

public class FileLogger : ILogger
{
    public void LogInfo(string message)  => /* Запис у файл */;
    public void LogError(string message) => /* Запис у файл */;
}

Далі клієнтський код виглядає так (неважливо, з яким логером він працює):


void DoWork(ILogger logger)
{
    logger.LogInfo("Робота почалася.");
    // ... якась робота ...
    logger.LogError("Робота пішла не так.");
}

Ось це і є програмування на рівні інтерфейсів: клієнтський код залежить тільки від контракту (інтерфейсу), а не від конкретної реалізації.

7. Помилки і типові пастки

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

  • Складно щось замінити чи протестувати — потрібно переписувати багато коду.
  • Впроваджувати нові типи не виходить без правок у багатьох місцях.
  • Модулі виявляються сильно «зчепленими» один з одним.

Навпаки, якщо від самого початку будувати систему на інтерфейсах і передавати параметри через них, можна досягти слабкої зв’язаності та гнучкості.

Головна порада: намагайтеся сприймати взаємодію між модулями як взаємодію між контрактами, а не між конкретними реалізаціями.

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