Antoine Sauvinet — Technology Executive & Entrepreneur
FR | EN
Blog CV Contact

[EnhanceYourCode] : le Builder Pattern, Partie 2

Builder

Bonjour,

Dans l’article précédent, nous avons exploré la théorie du Builder pattern.

Voyons un exemple plus concret :

Supposons que nous construisons le modèle de base d’un jeu de rôle (RPG). Voici les règles de base :

  • Un joueur peut être un Héros : un Guerrier, un Mage, ou un Voleur (on garde ça simple)
  • Chaque Héros a 4 caractéristiques principales : Santé, Force, Esprit et Vitesse, comptées en points.
  • Les Héros ont un Niveau, et les caractéristiques de départ sont basées sur ce niveau (Santé commence à Niveau * 10, Force et Esprit commencent à Niveau * 5, et Vitesse commence à Niveau * 3)
  • Le Guerrier a un Modificateur (+2 Force, -2 Esprit), le Mage a un Modificateur (+2 Esprit, -2 Force)
  • Le joueur peut améliorer 2 Caractéristiques de 1 point chacune ou 1 caractéristique de 2 points, afin de personnaliser son Héros.

Une implémentation naïve de la classe Hero serait :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
namespace Blog.RolePlayingGame.Core
{
    public interface ITarget
    {
        void ReceivePhysicalAttack(int strength);
        void ReceiveMagicalAttack(int strength);
    }

    public class Hero
    {
        public HeroClass Class { get; private set; }
        public string Name { get; private set; }
        public int Level { get; private set; }
        public int Health { get; private set; }
        public int Strength { get; private set; }
        public int Spirit { get; private set; }
        public int Speed { get; private set; }

        public Hero(HeroClass @class, string name, int level, int health, int strength, int spirit, int speed)
        {
            Class = @class;
            Name = name;
            Level = level;
            Health = health;
            Strength = strength;
            Spirit = spirit;
            Speed = speed;
        }

        public void Hit(ITarget target)
        {
            target.ReceivePhysicalAttack(this.Strength);
        }

        public void Spell(ITarget target)
        {
            target.ReceiveMagicalAttack(this.Spirit);
        }
    }

    public enum HeroClass
    {
        Warrior = 1,
        Wizard = 2,
        Thief = 3
    }
}

Concentrons-nous sur la Création du Héros : Tous les Héros sont du même type : En effet, malgré les différentes classes, chaque Héros a le même type de caractéristiques. Donc, il n’y a pas besoin d’une classe spécifique reflétant la “Classe du Héros”.

Voici un premier test (Notez que j’utilise le framework de test Fixie et la bibliothèque d’assertions Shouldly, je posterai à ce sujet bientôt) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
using Shouldly;

namespace Blog.RolePlayingGame.Core.Tests
{
    public class HeroBuilderTests
    {
        public void An_HeroBuilder_can_build_a_warrior()
        {
            Hero actual =  new HeroBuilder()
                                 .OfWarriorClass()
                                 .Create();

            actual.Class.ShouldBe(HeroClass.Warrior);
        }
    }
}

Nous voulons nous assurer que notre builder peut construire un guerrier. Donc l’implémentation est directe :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
namespace Blog.RolePlayingGame.Core
{
    public class HeroBuilder
    {
        private HeroClass _class;

        public HeroBuilder OfWarriorClass()
        {
            _class = HeroClass.Warrior;
            return this;
        }
        
        public Hero Create()
        {
            
            return new Hero(@class: _class, 
                name: _name ,
                level:1, 
                health: _health,
                strength: 0,
                spirit: 0,
                speed: 0);
        }
    }
    
        
}

Évidemment, nous pouvons ajouter les méthodes pour les autres classes (gardez à l’esprit que le périmètre est vraiment restreint). L’étape suivante serait de s’assurer que nous ne pouvons pas construire un Héros sans classe. Le test serait :

1
2
3
4
5
        public void An_HeroBuilder_cannot_build_a_hero_without_class()
        {
            Action tryToBuildAHeroWithoutClass = () => new HeroBuilder().Create();
            tryToBuildAHeroWithoutClass.ShouldThrow<HeroBuilder.BuildingHeroWithoutClassAttempException>();
        }

Donc nous mettons à jour le Builder en conséquence :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
namespace Blog.RolePlayingGame.Core
{
    public class HeroBuilder
    {
        private HeroClass _class;

        public HeroBuilder OfWarriorClass()
        {
            _class = HeroClass.Warrior;
            return this;
        }
        
        public Hero Create()
        {
           if( IsClassNotSettled())
               throw new BuildingHeroWithoutClassAttempException();
            
           return new Hero(@class: _class, 
                name: _name ,
                level:1, 
                health: _health,
                strength: 0,
                spirit: 0,
                speed: 0);
        }
        
        public class BuildingHeroWithoutClassAttempException : Exception
        {
            public BuildingHeroWithoutClassAttempException() : base ("Cannot creating an hero without class") { }
        }
    }
    
        
}

La garde se produit dans la méthode Create car c’est l’endroit le plus pratique pour la placer, pour le moment.

En suivant nos “Règles Métier”, nous aboutissons à ce type de classe :

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
using System;

namespace Blog.RolePlayingGame.Core
{
    public class HeroBuilder
    {
        private HeroClass _class;
        private int _level;
        private int _health;
        private int _strength;
        private int _spirit;
        private int _speed;
        private string _name;

        private CharacteristicsModificator _modificator;
        private readonly CharacteristicBoosterSet _boosterSet = new CharacteristicBoosterSet();

        private bool _dolevelComputation = true;

        public HeroBuilder()
        {
            _level = 1;
        }

        public HeroBuilder OfWarriorClass()
        {
            _class = HeroClass.Warrior;
            _modificator = new CharacteristicsModificator(strength: 2, spirit: -2);
            return this;
        }

        public HeroBuilder OfWizardClass()
        {
            _class = HeroClass.Wizard;
            _modificator = new CharacteristicsModificator(strength: -2, spirit: 2);
            return this;
        }

        public HeroBuilder OfThiefClass()
        {
            _class = HeroClass.Thief;
            _modificator = CharacteristicsModificator.Void;
            return this;
        }

        public HeroBuilder WithName(string name)
        {
            _name = name;
            return this;
        }

        public HeroBuilder WithLevel(int level)
        {
            _level = level;
            _health = _level * 10;
            _strength = _level * 5;
            _spirit = _level * 5;
            _speed = _level * 3;

            _dolevelComputation = false;
            return this;
        }

        public HeroBuilder BoostStrength(BoostCharacteristics boost = BoostCharacteristics.OfOne)
        {
            _boosterSet.BoostStrength(boost);
            return this;
        }

        public HeroBuilder BoostSpirit(BoostCharacteristics boost = BoostCharacteristics.OfOne)
        {
            _boosterSet.BoostSpirit(boost);
            return this;
        }

        public Hero Create()
        {
            if (IsClassNotSettled())
                throw new BuildingHeroWithoutClassAttempException();

            if (IsNameNotSettled())
                throw new BuildingHeroWithoutNameAttempException();

            if (_dolevelComputation)
                WithLevel(1);

            ApplyModificator();

            ApplyBoost();

            return new Hero(@class: _class,
                name: _name,
                level: _level,
                health: _health,
                strength: _strength,
                spirit: _spirit,
                speed: _speed);
        }

        private bool IsClassNotSettled()
        {
            return _class == default(HeroClass);
        }

        private bool IsNameNotSettled()
        {
            return string.IsNullOrWhiteSpace(_name);
        }

        private void ApplyModificator()
        {
            _strength += _modificator.Strength;
            _spirit += _modificator.Spirit;
        }

        private void ApplyBoost()
        {
            _strength += _boosterSet.StrengthBoost;
            _spirit += _boosterSet.SpiritBoost;
        }

        public class BuildingHeroWithoutClassAttempException : Exception
        {
            public BuildingHeroWithoutClassAttempException() : base("Cannot creating an hero without class") { }
        }

        public class BuildingHeroWithoutNameAttempException : Exception
        {
            public BuildingHeroWithoutNameAttempException() : base("Cannot creating an hero without name") { }
        }
    }
}

Nous avons en fait construit un Domain-Specific-Language (DSL) pour notre contexte de Création de Héros. Cela peut sembler un peu complexe pour l’objectif à première vue, mais nous avons atteint une séparation complète entre la complexité de construire un Héros et le comportement du Héros plus tard dans le jeu. Pour illustrer cela, nous pouvons regarder une implémentation potentielle d’un client de jeu :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
using System;

namespace Blog.RolePlayingGame.Core
{
    class Program
    {
        static void Main(string[] args)
        {
            var myHero = new HeroBuilder()
                                .OfWarriorClass()
                                .WithName("Mighty Hall-Dard")
                                .WithLevel(2)
                                .BoostStrength()
                                .BoostSpirit()
                                .Create();

            var enemy = new Monster();

            myHero.Hit(enemy);
        }
     }

    class Monster : ITarget
    {
        private int health = 15;
        private int _strength = 3;
        private int _spirit = 3;

        public void ReceivePhysicalAttack(int incomingStrength)
        {
            health -= Math.Max(0, (incomingStrength - _strength));
        }

        public void ReceiveMagicalAttack(int strength)
        {
            throw new NotImplementedException();
        }
    }
}

Dans cet article, nous avons vu comment implémenter le Builder Design Pattern en C# avec une approche Fluent Interface.

Vous pouvez trouver le code source dans ce repository github

Commentaires

💬 Les commentaires sont partagés entre toutes les versions linguistiques de cet article.