Интерфейсы (Interfaces) в C#: Фундамент гибкой архитектуры

Интерфейсы Фундамент гибкой архитектуры

Интерфейс — это контракт (договор). Он говорит: «Я гарантирую, что у этого объекта будут вот такие методы и свойства», но не говорит, как именно они работают.

В архитектуре игр (особенно стратегий) интерфейсы — это главный инструмент для создания кода, который легко расширять, тестировать и поддерживать.

Простая аналогия

Представьте розетку.

  • Интерфейс: Форма дырок и напряжение (220В).
  • Реализация: В розетку можно включить чайник, лампу, зарядку или компьютер.
  • Выгода: Вам не нужно менять проводку в стене, чтобы включить новый прибор. Главное, чтобы у прибора была подходящая вилка (реализация интерфейса).

В коде:

  • Интерфейс: IDamageable (получить урон).
  • Реализация: Soldier, Building, Tree (все могут получать урон, но по-разному).

Синтаксис в C#

Интерфейсы начинаются с буквы I (по соглашению) и содержат только подписи методов (без тела).

C#
// 1. Объявление интерфейса
public interface IDamageable
{
    void TakeDamage(int amount); // Только подпись, без кода!
    int Health { get; } // Только свойство
}

// 2. Реализация интерфейса в классе
public class Soldier : MonoBehaviour, IDamageable
{
    public int Health { get; private set; } = 100;

    public void TakeDamage(int amount)
    {
        Health -= amount;
        if (Health <= 0) Die();
    }
    
    void Die() { /* Логика смерти солдата */ }
}

public class Building : MonoBehaviour, IDamageable
{
    public int Health { get; private set; } = 1000;

    public void TakeDamage(int amount)
    {
        Health -= amount;
        if (Health <= 0) DestroyBuilding();
    }
    
    void DestroyBuilding() { /* Логика разрушения здания */ }
}

Зачем это нужно в вашей игре?

Три главные причины:

Причина 1: Полиморфизм (Единое управление разными объектами)

Например, если у вас игра стратегия. В ней сотни типов объектов: юниты, здания, ресурсы, декорации.
Без интерфейсов вам пришлось бы писать так:

C#
// ❌ ПЛОХО: Много проверок типов
if (obj is Soldier) { ((Soldier)obj).TakeDamage(10); }
else if (obj is Building) { ((Building)obj).TakeDamage(10); }
else if (obj is Resource) { ((Resource)obj).TakeDamage(10); }

С интерфейсом код становится универсальным:

C#
// ✅ ХОРОШО: Работает для любого объекта с интерфейсом
public void ApplyExplosionDamage(Vector3 center, int damage)
{
    var hitObjects = Physics.OverlapSphere(center, radius);
    
    foreach (var hit in hitObjects)
    {
        // Получаем компонент, реализующий интерфейс
        var damageable = hit.GetComponent<IDamageable>(); 
        
        if (damageable != null)
        {
            damageable.TakeDamage(damage); // Не важно, кто это: танк или стена
        }
    }
}

Выгода: Вы можете добавить новый тип объекта (например, Hero), реализовать у него IDamageable, и взрывы автоматически начнут наносить ему урон без изменения кода взрыва.

Причина 2: Абстракция Бэкенда (PlayFab vs Свой сервер)

Не важно какой у вас бекенд, например PlayFab и свой сервер. Интерфейсы позволяют не зависеть от выбора.

C#
// 1. Создаем контракт для данных
public interface IDataService
{
    void SavePlayerProgress(PlayerData data);
    void LoadPlayerProgress(System.Action<PlayerData> callback);
}

// 2. Реализация для PlayFab
public class PlayFabService : IDataService
{
    public void SavePlayerProgress(PlayerData data) { /* Код PlayFab SDK */ }
    public void LoadPlayerProgress(System.Action<PlayerData> callback) { /* Код PlayFab SDK */ }
}

// 3. Реализация для своего сервера (ASP.NET)
public class CustomApiService : IDataService
{
    public void SavePlayerProgress(PlayerData data) { /* Код UnityWebRequest к вашему API */ }
    public void LoadPlayerProgress(System.Action<PlayerData> callback) { /* Код UnityWebRequest */ }
}

// 4. Использование в игре
public class GameManager : MonoBehaviour
{
    // Мы зависим от интерфейса, а не от конкретного класса
    private IDataService _dataService; 

    void Start()
    {
        // Можно легко поменять реализацию в одном месте
        _dataService = new PlayFabService(); 
        // Или: _dataService = new CustomApiService();
        
        _dataService.LoadPlayerProgress(OnDataLoaded);
    }
}

Выгода: PlayFab конечно наладить проще, но если вдруг он станет дорогим или вы решите писать свой сервер, вы меняете одну строку в коде, а не переписываете всю игру.

Причина 3: Тестируемость (Mocking)

Интерфейсы позволяют тестировать логику без реального сервера или рекламы.

C#
// Интерфейс для рекламы
public interface IAdService
{
    void ShowInterstitial();
}

// Реализация для продакшена (реальная реклама)
public class UnityAdsService : IAdService { /* Реальный SDK */ }

// Реализация для тестов (пустышка)
public class MockAdService : IAdService 
{ 
    public void ShowInterstitial() { Debug.Log("Реклама показана (тест)"); } 
}

Вы можете запустить игру в редакторе с MockAdService, чтобы не ждать загрузки реальной рекламы каждый раз.

Интерфейс vs Абстрактный класс

Частый вопрос: чем отличается interface от abstract class?

Характеристика
Интерфейс (interface)
Абстрактный класс (abstract class)
Суть
«Что объект может делать» (Capability)
«Кем объект является» (Identity)
Наследование
Класс может реализовать множество интерфейсов
Класс может наследовать только один класс
Код
Только подписи (обычно)
Может содержать готовый код и поля
Пример
IDamageable, IMovable, ISaveable
Character, Building, Unit

Правило:

  • Используйте Абстрактный класс, если у объектов много общего кода (например, у всех юнитов есть здоровье, позиция, анимация).
  • Используйте Интерфейс, если нужно добавить способность разным объектам (например, ISellable — можно продать и здание, и юнита, и ресурс).
C#
// Комбинированный подход
public abstract class Unit : MonoBehaviour 
{
    public int Health; // Общие данные
    protected void Move() { /* Общая логика */ }
}

// Юнит может быть и[Unit], и [IDamageable], и [IMovable]
public class Archer : Unit, IDamageable, ITargetable 
{
    // Реализация методов интерфейсов
}

Принципы правильной работы с интерфейсами

Interface Segregation Principle (ISP)

Не создайте «жирные» интерфейсы. Лучше много маленьких, чем один большой.

  • ❌ Плохо: IGameObject (содержит методы Move, Shoot, Build, Sell, Talk).
  • ✅ Хорошо: IMovable, IAttacker, IBuildable, ISellable.

Так вам не придется реализовывать метод Shoot() для здания, которое стрелять не может.

Dependency Injection (Внедрение зависимостей)

Не создавайте объекты внутри класса через new. Передавайте их через конструктор или поле.

C#
// ❌ Жесткая связность
public class Shop : MonoBehaviour
{
    private PlayFabService _service = new PlayFabService(); // Привязка к конкретному классу
}

// ✅ Гибкая архитектура
public class Shop : MonoBehaviour
{
    private IDataService _service; // Зависимость от интерфейса

    public void Init(IDataService service)
    {
        _service = service;
    }
}

В Unity это часто делается через Awake или внешний «Composer» класс.

Именование

Всегда используйте префикс I.

  • IInventory
  • INetworkManager
  • IQuest

Это сразу говорит другому разработчику: «Это интерфейс, здесь нет реализации».

Пример архитектуры для стратегии

Вот как интерфейсы связывают Клиент, Экономику и Бэкенд:

C#
// 1. Интерфейсы (Чистая архитектура)
public interface ICurrencyService { void AddGold(int amount); int GetGold(); }
public interface IBuildingSystem { void StartConstruction(BuildingType type); }
public interface INotificationService { void Show(string message); }

// 2. Реализации (Инфраструктура)
public class PlayFabCurrencyService : ICurrencyService { /* ... */ }
public class LocalBuildingSystem : IBuildingSystem { /* ... */ }
public class UINotificationService : INotificationService { /* ... */ }

// 3. Использование (Геймплей)
public class CityManager : MonoBehaviour
{
    private ICurrencyService _currency;
    private IBuildingSystem _building;
    private INotificationService _notify;

    // Внедряем зависимости извне
    public void Init(ICurrencyService currency, IBuildingSystem building, INotificationService notify)
    {
        _currency = currency;
        _building = building;
        _notify = notify;
    }

    public void OnBuildButtonClicked()
    {
        if (_currency.GetGold() >= 100)
        {
            _currency.AddGold(-100);
            _building.StartConstruction(BuildingType.Barracks);
            _notify.Show("Стройка началась!");
        }
        else
        {
            _notify.Show("Недостаточно золота!");
        }
    }
}

Что это дает:

  1. CityManager не знает про PlayFab. Ему всё равно, откуда берутся деньги.
  2. Вы можете заменить PlayFabCurrencyService на ServerCurrencyService без изменения CityManager.
  3. Вы можете легко протестировать CityManager, передав ему MockCurrencyService.

Подводные камни

  1. Нельзя serialize в Inspector: Поля типа interface не отображаются в Инспекторе Unity.
    • Решение: Используйте поля конкретного типа (MonoBehaviour) для ссылки в инспекторе, а в коде приводите к интерфейсу, либо используйте систему Dependency Injection (например, Zenject/VContainer).
  2. Производительность: Вызов методов через интерфейс чуть медленнее, чем прямой вызов (виртуальный вызов).
    • Реальность: В стратегиях это незаметно. Не оптимизируйте это, если у вас не тысячи вызовов в кадр.
  3. Over-engineering: Не создавайте интерфейс для каждого класса. Если класс один и не будет заменен — интерфейс не нужен.

Итог

Если вы делаете онлайн-стратегию интерфейсы критически важны в трех местах:

  1. Взаимодействие с бэкендом: (IDataService) — чтобы легко менять провайдера (PlayFab ↔ Свой сервер).
  2. Взаимодействие объектов на карте: (IDamageable, IInteractable) — чтобы универсализировать логику кликов и боя.
  3. Монетизация и сервисы: (IAdService, IAnalyticsService) — чтобы легко отключать рекламу или менять SDK.

Используйте интерфейсы там, где вы ожидаете изменения реализации в будущем. Это спасёт вас от переписывания всего проекта через год.

Оцените статью
Unity Learn
Добавить комментарий