Press "Enter" to skip to content

Lesson 04.5: Improving the World – Factory and Guard Clauses

In this lesson, we will do more refactoring to the code we wrote over the last few lessons, where we were building the world classes.

Named Parameters

In the GameSession constructor, we instantiate a Player object, and set its properties.

Because the Player class has public properties, that each have a public “set”, we can make this code cleaner, by using “named parameters”.

When we use named parameters, we can remove the parentheses after the class name, when we instantiate the object. Then, we add opening and closing curly braces. Inside the curly braces, Visual Studio will show us the public properties that we can assign values to. We add the property name, an equal sign, and the value we want to assign to the property. If we want to add values for more than one property, we need to add a comma between each property.

Step 1: Edit GameSession.cs

In the constructor, replace this code:

CurrentPlayer = new Player();
CurrentPlayer.Name = "Scott";
CurrentPlayer.CharacterClass = "Fighter";
CurrentPlayer.HitPoints = 10;
CurrentPlayer.Gold = 1000000;
CurrentPlayer.ExperiencePoints = 0;
CurrentPlayer.Level = 1;

With this code:

CurrentPlayer = new Player
                {
                    Name = "Scott",
                    CharacterClass = "Fighter",
                    HitPoints = 10,
                    Gold = 1000000,
                    ExperiencePoints = 0,
                    Level = 1
                };

I like this technique because it is very clear what properties are being set. Plus, IntelliSense is available, as you set the property values.

Static Factory Class

While we are inside the GameSession constructor, we can make an improvement to the WorldFactory class.

Notice that we instantiate a WorldFactory object, get a World object from the CreateWorld function, and never use the WorldFactory object again.

We can make a change that will allow us to get a World object from the WorldFactory class, without instantiating a WorldFactory object. We do this by making the WorldFactory “static”.

You can use a static class, without instantiating it. Because we are only going to use the WorldFactory class to create a World object, it is a good class to make static. Using a static class, or function, to create objects is known as the Factory Design Pattern.

There are some rules to remember about static things.

  1. A static class can only have static methods/functions.
  2. An instanced class (like the Player class) can have static functions. These functions can be called, without creating an instance of the class.
  3. A static function can only use objects it creates inside the function, other static functions, or static class-level variables.

Step 1: Edit WorldFactory.cs

Change this line:

internal class WorldFactory

To this:

internal static class WorldFactory

Notice that the CreateWorld function now has red squiggly lines underneath it. If you hover over CreateWorld, you’ll see an error message that says:

‘CreateWorld’: cannot declare instance members in a static class

Static class ‘Engine.Factories.WorldFactory’ cannot have a non-static method ‘World CreateWorld()’

This error is saying, “because the WorldFactory class is static (it will never be instantiated), it cannot have a function that requires an instantiated object”.

To fix this, we will also make the CreateWorld function static, by changing it to this:

internal static World CreateWorld()

Now, if we look at the constructor in GameSession, we see some errors. That’s because we are trying to instantiate a WorldFactory object – which we cannot do, now that it is a static class.

So, we will change these lines:

WorldFactory factory = new WorldFactory();
CurrentWorld = factory.CreateWorld();

To this:

CurrentWorld = WorldFactory.CreateWorld();

Now, the program doesn’t create an object it will only use one time.

NOTE: We can safely make WorldFactory a static class because we are not “maintaining state” in it – we are not holding any variable values inside the static class. We cannot make the Player class static because we need to “maintain state”. In a Player object, we store the player’s hit points, experience points, gold, etc. Those values will change, as the user plays the game.

You can store values in a static class, by creating static variables (like the private variables we use in the Player class). However, then you need to be more cautious about what changes those values. Because a static class does not need to be instantiated, it is available everywhere in the program. When any part of a program can change a value, it can be difficult to track down bugs.

Guard Clauses

Finally, we will look at the movement functions in GameSession.cs.

Currently, MoveNorth (and the other movement functions) will always try to move the player – even if the world does not have a location in the direction of the function. We prevent that from happening by making the movement buttons invisible in the UI. However, there is a saying in programming, “Never trust user input”. In some programs, a hacker could figure out how to send invalid calls to our code. Even though this is not a commercial program, we should be safer.

Inside each of the movement functions, we will do a check if there is a location in the direction. If so, we will set the CurrentLocation to the new location. If not, we will not do anything.

So, we will change the code for the movement functions to this:

public void MoveNorth()
{
    if(HasLocationToNorth)
    {
        CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate + 1);
    }
}

public void MoveEast()
{
    if(HasLocationToEast)
    {
        CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate + 1, CurrentLocation.YCoordinate);
    }
}

public void MoveSouth()
{
    if(HasLocationToSouth)
    {
        CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate - 1);
    }
}

public void MoveWest()
{
    if(HasLocationToWest)
    {
        CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate - 1, CurrentLocation.YCoordinate);
    }
}

Now, if the movement button visibility does not work, we have a second layer of protection in our code – which should prevent the user from doing something that would cause an error.

Final Version of the Source Code

WorldFactory.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Engine.Models;

namespace Engine.Factories
{
    internal static class WorldFactory
    {
        internal static World CreateWorld()
        {
            World newWorld = new World();

            newWorld.AddLocation(-2, -1, "Farmer's Field",
                "There are rows of corn growing here, with giant rats hiding between them.",
                "/Engine;component/Images/Locations/FarmFields.png");

            newWorld.AddLocation(-1, -1, "Farmer's House",
                "This is the house of your neighbor, Farmer Ted.",
                "/Engine;component/Images/Locations/Farmhouse.png");

            newWorld.AddLocation(0, -1, "Home",
                "This is your home",
                "/Engine;component/Images/Locations/Home.png");

            newWorld.AddLocation(-1, 0, "Trading Shop",
                "The shop of Susan, the trader.",
                "/Engine;component/Images/Locations/Trader.png");

            newWorld.AddLocation(0, 0, "Town square",
                "You see a fountain here.",
                "/Engine;component/Images/Locations/TownSquare.png");

            newWorld.AddLocation(1, 0, "Town Gate",
                "There is a gate here, protecting the town from giant spiders.",
                "/Engine;component/Images/Locations/TownGate.png");

            newWorld.AddLocation(2, 0, "Spider Forest",
                "The trees in this forest are covered with spider webs.",
                "/Engine;component/Images/Locations/SpiderForest.png");

            newWorld.AddLocation(0, 1, "Herbalist's hut",
                "You see a small hut, with plants drying from the roof.",
                "/Engine;component/Images/Locations/HerbalistsHut.png");

            newWorld.AddLocation(0, 2, "Herbalist's garden",
                "There are many plants here, with snakes hiding behind them.",
                "/Engine;component/Images/Locations/HerbalistsGarden.png");

            return newWorld;
        }
    }
}
GameSession.cs
using Engine.Factories;
using Engine.Models;

namespace Engine.ViewModels
{
    public class GameSession : BaseNotificationClass
    {
        private Location _currentLocation;

        public World CurrentWorld { get; set; }
        public Player CurrentPlayer { get; set; }

        public Location CurrentLocation
        {
            get { return _currentLocation; }
            set
            {
                _currentLocation = value;

                OnPropertyChanged(nameof(CurrentLocation));
                OnPropertyChanged(nameof(HasLocationToNorth));
                OnPropertyChanged(nameof(HasLocationToEast));
                OnPropertyChanged(nameof(HasLocationToWest));
                OnPropertyChanged(nameof(HasLocationToSouth));
            }
        }

        public bool HasLocationToNorth
        {
            get
            {
                return CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate + 1) != null;
            }
        }

        public bool HasLocationToEast
        {
            get
            {
                return CurrentWorld.LocationAt(CurrentLocation.XCoordinate + 1, CurrentLocation.YCoordinate) != null;
            }
        }

        public bool HasLocationToSouth
        {
            get
            {
                return CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate - 1) != null;
            }
        }

        public bool HasLocationToWest
        {
            get
            {
                return CurrentWorld.LocationAt(CurrentLocation.XCoordinate - 1, CurrentLocation.YCoordinate) != null;
            }
        }

        public GameSession()
        {
            CurrentPlayer = new Player
                            {
                                Name = "Scott",
                                CharacterClass = "Fighter",
                                HitPoints = 10,
                                Gold = 1000000,
                                ExperiencePoints = 0,
                                Level = 1
                            };

            CurrentWorld = WorldFactory.CreateWorld();

            CurrentLocation = CurrentWorld.LocationAt(0, 0);
        }

        public void MoveNorth()
        {
            if(HasLocationToNorth)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate + 1);
            }
        }

        public void MoveEast()
        {
            if(HasLocationToEast)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate + 1, CurrentLocation.YCoordinate);
            }
        }

        public void MoveSouth()
        {
            if(HasLocationToSouth)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate - 1);
            }
        }

        public void MoveWest()
        {
            if(HasLocationToWest)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate - 1, CurrentLocation.YCoordinate);
            }
        }
    }
}
Leave a Reply

Your email address will not be published. Required fields are marked *