C# from Basics to Mastery — Part 4
Divide roles into inheritance, interfaces, and generics
The articles written on this blog were created with the goal of teaching. Also, the information in the article may be incorrect, so please be careful.
Inheritance and interfaces can be used when multiple classes need to provide the same behavior. What kind of contract is presented to the outside world is more important than tying things in common.
Expressing roles through interfaces
public interface IDamageable
{
bool IsAlive { get; }
void TakeDamage(int amount);
}
public sealed class Monster : IDamageable
{
public int Health { get; private set; } = 100;
public bool IsAlive => Health > 0;
public void TakeDamage(int amount)
{
Health = Math.Max(0, Health - amount);
}
}
For the attack code, you only need to know IDamageable, not the entire Monster. You can use the same code later to add training scarecrows or destructible boxes.
Inheritance is used in is-a relationships
public abstract class Skill
{
public string Name { get; }
protected Skill(string name) => Name = name;
public abstract void Execute(IDamageable target);
}
public sealed class FireSkill : Skill
{
public FireSkill() : base("Fire") { }
public override void Execute(IDamageable target)
{
target.TakeDamage(30);
}
}
Inheritance also inherits the state and rules of the base class. If you create a deep inheritance structure simply to reuse a few lines of code, changes to the parent will affect all children.
Maintaining type safety with generics
public sealed class Registry<T> where T : class
{
private readonly Dictionary<int, T> values = new();
public void Add(int id, T value) => values.Add(id, value);
public T? Find(int id)
{
return values.TryGetValue(id, out T? value) ? value : null;
}
}
Registry<Monster> and Registry<Skill> use the same storage rules, but their values cannot be mixed. The constraint where tells the compiler what properties T must provide.
Interfaces express roles, inheritance expresses a common base, and generics express the same rules for different types. Rather than using many of the three functions at once, it is safer to create the smallest contract first and expand when necessary.