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 :
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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):
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 :
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 :
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 :
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 :
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 :
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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