Express type composition in OOP

Viewed 113

Suppose we want to write a RPG game which has 3 base character types: Fighter, Mage and Archer. Also we have combined character types like Knight, Paladin and Ranger. In TypeScript we can describe interfaces for characters like this:

    // fighter can fight
    // mage can cast
    // archer can shoot
    // paradin can cast and fight
    // knight can fight and shoot
    // ranger can cast and shoot

    type Fighter = {fight: () => string}
    type Mage = {cast: () => string}
    type Archer = {shoot: () => string}
    type Knight = Fighter & Archer
    type Paladin = Fighter & Mage
    type Ranger = Archer & Mage

The question is how to implement these interfaces using only OOP techniques (imagine that we are working with mainstream OOP language like C#) ?

I had no success expressing this with inheritance without behavior duplication. Currently the only way I see is:

class Paladin {
  constructor(private fighter: Fighter, private mage: Mage) {}

  fight() {this.fighter.fight()}
  cast() {this.mage.cast()}
}

But I don't like that approach because any time I want to create a paladin I also have to create a mage and a fighter which is basically the same person

3 Answers

Maybe try to push your composition a little bit further. You only really need one type with different abilities, for example:

public class Hero
{
    private readonly string name;
    private readonly IEnumerable<IHeroAbility> abilities;

    public Hero(string name, [Optional] IEnumerable<IHeroAbility> abilities)
    {
        this.name = name;
        this.abilities = abilities ?? new List<IHeroAbility> { new OhCrap() };
    }

    public void Attack()
    {
        Console.WriteLine(name);

        foreach (var ability in abilities) ability.Execute();

        Console.WriteLine();
    }
}

Now prepare the abilities that will be available:

public interface IHeroAbility
{
    void Execute();
}

public class SwingSwordAbility : IHeroAbility
{
    public void Execute()
    {
        Console.WriteLine("And with all his might the hero swung his sword down onto his opponent...");
    }
}

public class ShootArrowAbility : IHeroAbility
{
    public void Execute()
    {
        Console.WriteLine("The hero loads an arrow into his bow without blinking and launches it with fierce speed towards his opponant...");
    }
}

public class CastFireBallAbility : IHeroAbility
{
    public void Execute()
    {
        Console.WriteLine("The hero reaches deep into the underverse and hurls a ball of fire towards his opponant...");
    }
}

public class OhCrap : IHeroAbility
{
    public void Execute()
    {
        Console.WriteLine("The hero pats his pockets as if he forgot something at home...");
    }
}

Then compose your hero types:

class Program
{
    static void Main(string[] args)
    {
        IHeroAbility swingSwordAbility = new SwingSwordAbility();
        IHeroAbility shootArrowAbility = new ShootArrowAbility();
        IHeroAbility castFireBallAbility = new CastFireBallAbility();

        var fighter = new Hero("The Fighter", new[] { swingSwordAbility });
        fighter.Attack();

        var mage = new Hero("The Mage", abilities: new[] { castFireBallAbility });
        mage.Attack();

        var archer = new Hero("The Archer", abilities: new[] { shootArrowAbility });
        archer.Attack();

        var knight = new Hero("The Knight", abilities: new[] { swingSwordAbility, shootArrowAbility });
        knight.Attack();

        var paladin = new Hero("The Paladin", abilities: new[] { swingSwordAbility, castFireBallAbility });
        paladin.Attack();

        var ranger = new Hero("The Ranger",abilities: new[] { shootArrowAbility, castFireBallAbility });
        ranger.Attack();

    }
}

If you want more explicit action groups you could modify your Hero class to something like this:

public class Hero
{
    private readonly string name;
    private readonly IEnumerable<IHeroAbility> meleeAbilities;
    private readonly IEnumerable<IHeroAbility> magicAbilities;
    private readonly IEnumerable<IHeroAbility> rangedAbilities;

    public Hero(
        string name,
        [Optional] IEnumerable<IHeroAbility> meleeAbilities,
        [Optional] IEnumerable<IHeroAbility> magicAbilities,
        [Optional] IEnumerable<IHeroAbility> rangedAbilities)
    {
        this.name = name;

        var defaultAbilities = new List<IHeroAbility> { new OhCrap() };
        this.meleeAbilities = meleeAbilities ?? defaultAbilities;
        this.magicAbilities = magicAbilities ?? defaultAbilities;
        this.rangedAbilities = rangedAbilities ?? defaultAbilities;
    }

    public void MeleeAttack()
    {
        var ability = meleeAbilities.First();
        Console.WriteLine(name);
        ability.Execute();
    }

    public void MagicAttack()
    {
        var ability = magicAbilities.First();
        Console.WriteLine(name);
        ability.Execute();
    }

    public void RangedAttack()
    {
        var ability = rangedAbilities.First();
        Console.WriteLine(name);
        ability.Execute();
    }

}

You could then start building it out to incorporate more sophisticated logic like having costs associated with abilities, cooldowns, ability selection etc. etc.

Hope this helps!

Your Paladin class is closed to the Composite design pattern and can be translated to C# as it is, perhaps with some readonly here and there to improve it a bit.

Using Composition (here by Forwarding) is considered a stronger design than one using inheritance (see Composition over inheritance) but it may feel less natural, which is why IMO you said:

But I don't like that approach because any time I want to create a paladin I also have to create a mage and a fighter which is basically the same person.

"The same person" is semantically more related to inheritance than composition but leads to more brittle design. Thinking in terms of roles or abilities (*) is more flexible: Healer, Druid is not a person but an ability a person/player/character can have.

(*) Term taken from Albert's answer but I still prefer your solution much simpler and explicit. To go further, we need a list of reference scenarios and/or a running prototype (or any existing program) to assess the design and the code...

For this purpose, I recommend using "Decorator design pattern".

you can make your main character after that add some specific behavior with decorators to them. for example :

var  Fighter = new Charactermaker();
var  Mage  = new Charactermaker();
var  Archer = new Charactermaker();

KnifeDecorator addKnight = new KnifeDecorator(Fighter);
Paladin Decorator addPaladin= new PaladinDecorator(addKnight);

and I have some advice for you. You can also use a "flyweight designs pattern" to make your application light. It lets you fit more objects into the available amount of RAM by sharing common parts of the state between multiple objects instead of keeping all of the data in each object.

for more information you can see flowing link :

Decorator Design Pattern

Related