levelonehuman
levelonehuman

Reputation: 1505

Class Design - property of several possible types

Take a class Player, which (currently) has a property of type Role:

public class Player
{
    public string Name { get; set; }
    public int Health { get; protected set; }
    public Role Role { get; set; }
}

Where Role holds a few distinct values:

public class Role
{
    public double DamageModifier { get { return _damageModifier; } }
    public double DefenseModifier { get { return _defenseModifier; } }
    public double HealthModifier { get { return _healthModifier; } }

    private double _damageModifier;
    private double _defenseModifier;
    private double _healthModifier;

    public Role(double damageMod, double defenseMod, double healthMod)
    {
        _damageModifier = damageMod;
        _defenseModifier = defenseMod;
        _healthModifier = healthMod;
    }
}

And there are a few "classes" that derive from Role:

public class Archer : Role
{
    private const double _damageModifier = 1.2;
    private const double _defenseModifier = 0.9;
    private const double _healthModifier = 0.8;

    public Archer() : base(_damageModifier, _defenseModifier, _healthModifier) { }
}

public class Mage : Role
{
    private const double _damageModifier = 1.4;
    private const double _defenseModifier = 0.7;
    private const double _healthModifier = 0.9;

    public Mage() : base(_damageModifier, _defenseModifier, _healthModifier) { }
}

public class Warrior : Role
{
    private const double _damageModifier = 1.0;
    private const double _defenseModifier = 1.2;
    private const double _healthModifier = 1.0;

    public Warrior() : base(_damageModifier, _defenseModifier, _healthModifier) { }
}

The end result of all this is really just that the Player.Role property is a placeholder for what will become an Archer, Mage, or Warrior. However, this specific subtype is decided at run-time (or more specifically, at character creation).

The issue I'm having arose from trying to serialize the player object to JSON data. When JSON serializes the Role property, it knows that it's a Role object and is able to get the corresponding xxModifier values from the instance, but I lose the fact that the Role property was a class derived from Role.

I'm obviously still learning, but this sort of issue kind of tells me that I've messed up in designing this class and it needs to be fixed. I could refactor the constructor so that Role holds a reference to the child class, but that seems wrong too.

EDIT Based on comments: My intention for the Archer/Mage/Warrior classes is that each class will have certain methods (abilities) available to it, in addition to the xxModifier properties that currently exist, such that a Warrior will do (comparatively) less damage but have higher defense and health, and the Role class currently serves as a way to give the Player object a place to hold his Role (RPG class). This specific implementation is my goal, and what's listed here is what I've come up with so far.

Upvotes: 3

Views: 94

Answers (2)

CodeCaster
CodeCaster

Reputation: 151654

You have two different problems here: specification and implementation.

In the specification, so in your game back-end, or hardcoded, or in your database or whatever, you want to be able to specify the attributes for various character classes for your player. This is fine.

That each specification holds different values does however not warrant a different implementation. You use subclassing to add behavior. You're not adding behavior, just altering existing behavior (I guess your non-shown damage calculation) slightly, based on the values of the player's role.

Furthermore, I now suspect you have in code something like this:

void NewPlayerButtonClicked(string characterClass)
{
    var player = new Player();

    if (characterClass == "Mage")
    {
        player.Role = new Mage();
    }

    // ...
}

That in and of itself is a design smell.

You just need a Role class to hold the values:

public class Role
{
    public string ClassName { get; set; }
    public decimal DamageModifier { get; set; }
    public decimal DefenseModifier { get; set; }
    public decimal HealthModifier { get; set; }
}

And in your player handling code you access the role's properties:

public decimal GetAttackDamage(Player attacker, Player defender, AttackInfo attack)
{
    // do calculations with attacker.Role.N and defender.Role.N
}

Now I guess you're asking this because you want to populate the fields while creating a character, or loading a game state. You can do that for example by storing your specification in JSON:

{
    "characterClasses" :    
    [
        {
            "className" : "Mage",
            "damageModifier" : 1.4,
            "defenseModifier" : 0.7,
            "healthModifier" : 0.9,
        },
        {
            ...
        }
    ]
}

Then if you save the player's character class, loading the data becomes very trivial:

player.Role = rolesLoadedFromJson.First(r => r.ClassName == player.CharacterClass);

But on all of this many books have been written, and the question is far too abstract (and the subject too broad) to give an in-depth answer on all aspects of object-oriented design.

Upvotes: 2

Matt Burland
Matt Burland

Reputation: 45155

To the immediate question of how to serialize and deserialize to JSON while retaining type, using something like JSON.Net, this is pretty trivial to do:

var p = new Player() { Role = new Mage() };
var json = JsonConvert.SerializeObject(p, new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.Objects });

Now you JSON will look something like this:

{
    "$type": "MyApp.Program+Player, MyApp",
    "Name": null,
    "
Health": 0,
    "Role": {
        "$type": "MyApp.Program+Mage, MyApp",
        "DamageModifier": 1.4,
        "DefenseModifier": 0.7,
        "HealthModifier": 0.9
    }
}

The additional $type properties are added so when you deserialize:

var d = (Player)JsonConvert.DeserializeObject(json, typeof(Player), new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.Objects });
var isMage = d.Role.GetType() == typeof(Mage);  // true

The question of whether or not your design is a good idea or not (as discussed in the comments) is a little broader.

Upvotes: 5

Related Questions