[EnhanceYourCode] : the Builder Pattern, Part2

Builder

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
}
}

view raw

Hero_naive.cs

hosted with ❤ by GitHub

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

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s