Hello,
In the previous article, we explored the theory of the builder pattern.
Let’s see a more concrete example :
Let’s assuming that we are building a Role Playing Game core model. Here are the basic rules:
- A player can be a Hero : a Warrior, a Wizard, or a Thief (we keep it simple)
- Every Hero has 4 main characteristics: Health, Strength, Spirit, and Speed, that are counted in points.
- Heroes have a Level, and starting characteristics are based on this level (Health starts at Level * 10, Strength and Spirit start at Level * 5, and Speed starts at Level * 3)
- Warrior has a (+2 Strength, -2 Spirit) Modificator, Wizard has (+2 Spirit, -2 Strength) Modificator
- Player can improve 2 Characteristics of 1 points each or 1 characteristic of 2 points, in order to cutomize his Hero.
A naive implementation of the Hero class would be :
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 | |
} | |
} |
Let’s focus on the Hero Creation : Every Heroes are of the same kind : Indeed, despite of the different classes, every Hero has the same kind of characteristics. So, there is no need for a specific class reflecting the “Hero Class”.
Here is a first test (Note that I use Fixie test framework and Shouldly assertion library, i’ll post about it soon):
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); | |
} | |
} | |
} |
We want to be sure that our builder can build a warrior. So the implementation is straigth-forward :
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); | |
} | |
} | |
} |
Obviously, we can add the methods for the other classes (keep in mind that the scope is really thin).
Next step would be to ensure we cannot build a Hero without a class. The test would be :
public void An_HeroBuilder_cannot_build_a_hero_without_class() | |
{ | |
Action tryToBuildAHeroWithoutClass = () => new HeroBuilder().Create(); | |
tryToBuildAHeroWithoutClass.ShouldThrow<HeroBuilder.BuildingHeroWithoutClassAttempException>(); | |
} |
So we update the Builder accordingly :
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") { } | |
} | |
} | |
} |
The guard occurs in the Create Method because it’s the most convenient place to place it, for the moment.
By following our “Business Rules”, we end-up with this kind of class :
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") { } | |
} | |
} | |
} |
We actually built a Domain-Specific-Language for our Hero Creation Context. This could seem a bit complex for the purpose at the first sight, but we do acheive a complete separation between the complexity of building a Hero and the behavior of the Hero later in the game. To illustrate this, we can take a look to a potential implementation of a game client :
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(); | |
} | |
} | |
} |
In this article, we saw how to implement the Builder Design Pattern in C# in a Fluent Interface way.
You can find the source code in this github repository