Часто нам не нужна никакая реализация. В случае с оружием, это не так. Нам очевидно, что всё оружие имеет боезапас (кроме пистолета), и наносит урон. Но иногда у нас абстракция не имеет никакой реализации (интерфейс) или имеет частичную (абстрактный класс с абстрактными методами). В обоих случаях нам при наследовании этих типов нужно их полностью реализовать.
Начнём с интерфейсов. Сразу стоит оговорить, что интерфейсы наследуются так же, как и обычные классы, но интерфейсов мы можем унаследовать сколько хотим, в отличие от обычных классов. Но интерфейсы также формируют тип, и с этим типом мы можем определять переменные, параметры, поля и многое другое.
Давайте представим, что у нашего игрока есть некоторый инвентарь. В нём могут быть размещены самые разные предметы, и мы можем с ними взаимодействовать. По сути взаимодействие с предметом сводится к тому, что:
- Мы помещаем предмет в список предметов в инвентаре.
- При помещении предмета в инвентарь, мы оповещаем его об этом.
- При выбросе из инвентаря оповещаем об этом.
Решить эту задачу мы можем так:
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. Часть методов у нас не имеют реализацию только, чтобы показать общий пример.
Эту абстракцию можно переделать по-разному. Например, мы понимаем, что почти все башни имеют задержку между выстрелами, и эта логика будет дублироваться, поэтому нам понадобится промежуточный класс. Также не все башни атакуют, некоторые замедляют цели, а некоторые работают только по дружественным целям и лечат их.