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

Часто нам не нужна никакая реализация. В случае с оружием, это не так. Нам очевидно, что всё оружие имеет боезапас (кроме пистолета), и наносит урон. Но иногда у нас абстракция не имеет никакой реализации (интерфейс) или имеет частичную (абстрактный класс с абстрактными методами). В обоих случаях нам при наследовании этих типов нужно их полностью реализовать.

Начнём с интерфейсов. Сразу стоит оговорить, что интерфейсы наследуются так же, как и обычные классы, но интерфейсов мы можем унаследовать сколько хотим, в отличие от обычных классов. Но интерфейсы также формируют тип, и с этим типом мы можем определять переменные, параметры, поля и многое другое.

Давайте представим, что у нашего игрока есть некоторый инвентарь. В нём могут быть размещены самые разные предметы, и мы можем с ними взаимодействовать. По сути взаимодействие с предметом сводится к тому, что:

  • Мы помещаем предмет в список предметов в инвентаре.
  • При помещении предмета в инвентарь, мы оповещаем его об этом.
  • При выбросе из инвентаря оповещаем об этом.

Решить эту задачу мы можем так:

1.54

class Gun : IInventoryHandler
{
    private int _bullets;
    protected float Damage;

    public virtual void Fire(Player player)
    {
        if (_bullets <= 0)
            return;

        player.ApplyDamage(Damage);
        _bullets--;
    }

    public void OnPickup()
    {
        //off scene model
    }

    public void OnDrop()
    {
        //on scene model
    }
}

interface IInventoryHandler
{
    void OnPickup();
    void OnDrop();
}

Мы просто создали тип IInventoryHandler, через ключевое слово interface. Такой тип может содержать часть функциональных членов: свойства, методы и индексаторы. Но при этом, эти члены не содержат реализации, они как бы пустые.

В дальнейшем класс или структура может наследовать этот интерфейс и реализовывать его. При наследовании интерфейса его необходимо полностью реализовать. Так, в классе Gun появились методы OnPickup и OnDrop, которые будут вызываться при подборе\выбросе этого предмета. У нас считается, что оружие 3D модель, и при вызове этих методов оружие включает\выключает свою модель.

Вероятней всего, у нас все предметы будут с 3D моделью, но предположим, что пока только оружие. Что до остальных предметов, поведение при подборе\выбросе другое.

Пример работы:

1.55

class Program
{
    static void Main(string[] args)
    {
        Gun gun = new Bow();
        Player player = new Player();

        player.PickUp(gun);
    }
}

class Player
{
    private float _health;
    private List<IInventoryHandler> _inventory = new List<IInventoryHandler>();

    public void ApplyDamage(float amount)
    {
        _health -= amount;
    }

    public void PickUp(IInventoryHandler item)
    {
        _inventory.Add(item);
        item.OnPickup();
    }
}

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

С помощью абстрактных классов удобно возводить каркасы для наследников. Внутри такого класса мы можем, например, описать порядок действий, и даже реализовать некоторые шаги, а наследников просим, как бы, подставить код в определённые точки.

Так мы закрепим шаги алгоритма в абстрактном классе, а в производных уже будем выполнять конкретные действия.

Давайте разберём пример, в котором у нас есть башни, которые ждут противника в зоне поражения, выбирают его как цель и начинают атаку. При этом наследники занимаются следующим:

  • Определяют можно ли атаковать эту цель;
  • Атакуют эту цель.

1.56

abstract class Tower
{
    private Player _target;

    public void Update()
    {
        if(_target == null)
        {
            foreach (var player in GetClosestPlayers())
            {
                if(CanAttack(player))
                {
                    _target = player;
                    break;
                }
            }
        }

        if (_target != null)
            Attack(_target);
    }

    protected abstract bool CanAttack(Player player);

    protected abstract void Attack(Player player);

    private IEnumerable<Player> GetClosestPlayers()
    {
        throw new NotImplementedException();
    }
}

class ArcherTower : Tower
{
    protected override void Attack(Player player)
    {
        player.ApplyDamage(2);
    }

    protected override bool CanAttack(Player player)
    {
        throw new NotImplementedException();
    }
}

Как вы видите, мы определили логику производных классов как абстрактные методы, а внутри производного класса ArcherTower реализовали абстрактные методы с помощью override. Часть методов у нас не имеют реализацию только, чтобы показать общий пример.

Эту абстракцию можно переделать по-разному. Например, мы понимаем, что почти все башни имеют задержку между выстрелами, и эта логика будет дублироваться, поэтому нам понадобится промежуточный класс. Также не все башни атакуют, некоторые замедляют цели, а некоторые работают только по дружественным целям и лечат их.

Если вы нашли ошибку, пожалуйста выделите её и нажмите Ctrl+Enter.


Leave a Reply

Your email address will not be published. Required fields are marked *