[S]OLID – The single responsibility principle
As the same suggests, The Single Responsibility Principle says to us that one entity should be responsible for just one functionality. Some people take this saying to the letter, but we need some common sense when writing code while thinking about the single responsibility of our classes. In other words, the scope of our classes will depend on our project scope.
I’m very acquainted with game development and the SRP is present in a huge part of this area. For example, let’s create a character for our game.
It’s not a compilable code, because it’s just for demonstration of the diagram above.
class MainCharacter
{
private:
Vector2D Position;
Weapon* CurrentWeapon;
Array<Projectile*> ShotProjectiles;
public:
MainCharacter()
{
Position = Vector2D(0,0);
CurrentWeapon = new Weapon0();
Input.Delegates.WhenClicked("Up", CharacterMovement::Jump);
Input.Delegates.WhenClicked("Right", CharacterMovement::MoveToFront);
Input.Delegates.WhenClicked("Left", CharacterMovement::MoveToBack);
Input.Delegates.WhenClicked("Space", CharacterMovement::Shoot);
}
void MoveToFront()
{
Position += Vector2D(10, 0);
}
void MoveToBack()
{
Position += Vector2D(-10, 0);
}
void Jump()
{
Position += Vector2D(0, 10);
}
void Shoot()
{
ShotProjectiles.Add(new Projectile());
}
void SelectNextWeapon()
{
delete Weapon;
if(CurrentWeapon->Name() == "Weapon0")
{
CurrentWeapon = new Weapon1();
}
else if(CurrentWeapon->Name() == "Weapon1")
{
CurrentWeapon = new Weapon2();
}
else if(CurrentWeapon->Name() == "Weapon2")
{
CurrentWeapon = new Weapon0();
}
}
void SelectPreviousWeapon()
{
delete Weapon;
if(CurrentWeapon->Name() == "Weapon0")
{
CurrentWeapon = new Weapon2();
}
else if(CurrentWeapon->Name() == "Weapon1")
{
CurrentWeapon = new Weapon0();
}
else if(CurrentWeapon->Name() == "Weapon2")
{
CurrentWeapon = new Weapon1();
}
}
virtual ~MainCharacter()
{
delete Weapon;
for(auto& d : ShotProjectiles) { delete d; };
}
}
Following the code, you can see that we have the MainCharacter
class doing a lot of things related to the weapon, input, and movement. For example:
- MainCharacter(): setting the initial position of the character, in the sequence, creating a default weapon class, (Weapon0, Weapon1, and Weapon2 are inherited from Weapon). This same constructor method is setting up all the necessary delegates to make the keyboard works with our functions.
- MoveFront(): Increment the position
- MoveBack(): Increment the position
- Jump(): Increment the position
- Shoot(): Create an object of type Projectile and add it to the array of projectiles.
- SelectNextWeapon(): Circulate the weapon classes in the forward direction
- SelectPreviousWeapon(): Circulate the weapon classes in the backward direction
All these functionalities are violating our SRP. We need to rethink the mode we made the MainCharacter
class. Look at the diagram below:
Now we have a much more organized scheme with some further details. The MainCharacter
no more is using resources from an external class like EngineInput
, because now the entity responsible for setting up the input delegates is the CharacterMovement
(maybe, in a very large scope/project, it will be necessary to split the CharacterMovement
in 2 parts, CharacterMovement
and CharacterInput
, this last one used just to handle the input delegates). As I said at the start of this post, all these decisions are strictly related to the size of your project or how much you need your project to be extensible/scalable.
Look at the CharacterWeapon
class, it is responsible just to store the current weapon that the character is using, and select the next or the previous one as well. The storage of how many projectiles were spawned or which projectile was spawned is now inside the SceneProjectileManager
.
The CharacterMovement now stores the character position property and all the motion methods related as well. The MainCharacter
class is working now as a Facade for the classes that are responsible for these other behaviors. To make our system responsible, we can wrap some important methods like GetCharacterPosition()
and GetCurrentWeapon()
in our MainCharacter
in order to access it fast.
Look at the code:
class CharacterMovement
{
private:
Vector2D Position;
public:
CharacterMovement()
{
Position = Vector2D(0,0);
Input.Delegates.WhenClicked("Up", CharacterMovement::Jump);
Input.Delegates.WhenClicked("Right", CharacterMovement::MoveToFront);
Input.Delegates.WhenClicked("Left", CharacterMovement::MoveToBack);
Input.Delegates.WhenClicked("Space", CharacterMovement::Shoot);
}
void MoveToFront()
{
Position += Vector2D(10, 0);
}
void MoveToBack()
{
Position += Vector2D(-10, 0);
}
void Jump()
{
Position += Vector2D(0, 10);
}
Vector2D GetPosition()
{
return Position;
}
virtual ~CharacterMovement();
}
class ProjectileManager
{
private:
Array<Projectile*> ShotProjectiles;
public:
ProjectileManager() = default;
AddProjectile(Vector2D Direction)
{
ShotProjectiles.Add(new Projectile(Direction));
}
virtual ~ProjectileManager()
{
for(auto& d : ShotProjectiles) { delete d; };
}
}
class CharacterWeapon
{
private:
ProjectileManager* _ProjectileManager;
Weapon* CurrentWeapon;
public:
CharacterWeapon()
{
_ProjectileManager = new _ProjectileManager();
}
void Shoot()
{
_ProjectileManager->AddProjectile(Vector2D::RandomDirection());
}
void SelectNextWeapon()
{
delete Weapon;
if(CurrentWeapon->Name() == "Weapon0")
{
CurrentWeapon = new Weapon1();
}
else if(CurrentWeapon->Name() == "Weapon1")
{
CurrentWeapon = new Weapon2();
}
else if(CurrentWeapon->Name() == "Weapon2")
{
CurrentWeapon = new Weapon0();
}
}
void SelectPreviousWeapon()
{
delete Weapon;
if(CurrentWeapon->Name() == "Weapon0")
{
CurrentWeapon = new Weapon2();
}
else if(CurrentWeapon->Name() == "Weapon1")
{
CurrentWeapon = new Weapon0();
}
else if(CurrentWeapon->Name() == "Weapon2")
{
CurrentWeapon = new Weapon1();
}
}
Weapon* GetWeapon()
{
return CurrentWeapon;
}
virtual ~CharacterWeapon()
{
delete _ProjectileManager;
}
}
class MainCharacter
{
public:
CharacterMovement* _CharacterMovement;
CharacterWeapon* _CharacterWeapon;
public:
MainCharacter()
{
_CharacterMovement = new CharacterMovement();
_CharacterWeapon = new CharacterWeapon();
}
Vector2D GetCharacterPosition()
{
return _CharacterMovement->GetPosition();
}
Weapon* GetCharacterWeapon()
{
return _CharacterWeapon->GetWeapon();
}
virtual ~MainCharacter()
{
delete _CharacterMovement;
delete _CharacterWeapon;
}
}
During our work, when we need some action from a weapon, for example, we need to access the CharacterWeapon object and invoke the desired method like this:
// Select the previous weapon
MainCharacterObject->_CharacterWeapon->SelectPreviousWeapon();
// Using our wrapped shortcut to get current weapon
Weapon* W = MainCharacterObject->GetCharacterWeapon();
Remember, all the code in the post wasn’t tested, it’s just to illustrate the manner you need to handle the classes to make them unique in terms of responsibility.