Home > Software engineering >  How to restructure nested if else statement
How to restructure nested if else statement

Time:09-04

Say you have the following table for a monster object:

Gender Age Type Result
Male Young Fire 1
Male Old Ice 2
Male Old Wood 6
Female Young Fire 4
Female Old Ice 5
Female Old Wood 8
Other Young Fire 7
Other Old Ice 10
Other Old Wood 20

If I were to have a monster and input it to get a result; how would I get the result of the monster object without making a long nested if-else statement?

if(monster.Gender == Male && monster.Age == Old)
    if(monster.Type == Ice)
        return 2
    if(monster.Type == Wood)
        return 6
if(monster.Gender == Male && monster.Age == Young)
etc...

I can't seem to wrap my head around making it extensable and such: say I want to add a column in the future or want to validate something before another thing.

Are there any simple design patterns or the like that I can implement to simplify it and make it extensible?

CodePudding user response:

You could use the switch expression introduced in C# 8.0 together with pattern matching.

Assuming these declarations:

public enum Gender
{
    Other,
    Female,
    Male
}

public enum Age
{
    Young,
    Old
}

public enum MonsterType
{
    Fire,
    Ice,
    Wood
}

class Monster
{
    public Gender Gender { get; set; }
    public Age Age { get; set; }
    public MonsterType Type { get; set; }
}

Using a tuple pattern:

return (monster.Gender, monster.Age, monster.Type) switch {
    (Gender.Male, Age.Old, MonsterType.Ice) => 2,
    (Gender.Male, Age.Old, MonsterType.Wood) => 6,
    (Gender.Male, Age.Young, MonsterType.Ice) => 12,
    ... etc.
    _ => -1
};

Using a property pattern:

return monster switch {
    { Gender: Gender.Male, Age: Age.Old, Type: MonsterType.Ice } => 2,
    { Gender: Gender.Male, Age: Age.Old, Type: MonsterType.Wood } => 6,
    { Gender: Gender.Male, Age: Age.Young, Type: MonsterType.Ice } => 12,
    ... etc.
    _ => -1
};

By adding this Deconstruct method to the Monster class...

public void Deconstruct(out Gender gender, out Age age, out MonsterType type)
{
    gender = Gender;
    age = Age;
    type = Type;
}

... we can use a positional pattern (looks like the tuple pattern; however, we can switch directly on monster instead of having to create a tuple first):

return monster switch {
    (Gender.Male, Age.Old, MonsterType.Ice) => 2,
    (Gender.Male, Age.Old, MonsterType.Wood) => 6,
    (Gender.Male, Age.Young, MonsterType.Ice) => 12,
    ... etc.
    _ => -1
};

We get this Deconstruct method for free if we declare the monster as record:

record Monster (Gender Gender, Age Age,  MonsterType Type);

A more object oriented approach would be to derive different monster classes from an abstract base class for different monster types instead of having a Type property. The monster classes would be responsible for their own Result value (whatever this represents. I will keep the name Result).

Then you only have to switch on Gender and Age.

The abstract base class (the enum MonsterType is obsolete now):

abstract class Monster
{
    public Gender Gender { get; set; }
    public Age Age { get; set; }

    public void Deconstruct(out Gender gender, out Age age)
    {
        gender = Gender;
        age = Age;
    }

    public abstract int Result { get; }
}

An implementation for the Fire type

class FireMonster : Monster
{
    public override int Result => this switch {
        (Gender.Male, Age.Young) => 1,
        (Gender.Female, Age.Young) => 4,
        (Gender.Other, Age.Young) => 7,
        // etc.
        _ => -1
    };
}

Note: if the cases cover all combinations of Gender and Age, i.e., if they are exhaustive, the default case _ => is not required anymore (if these properties are of an enum type. I they were int constants the C# compiler could not determine if they are exhaustive).

CodePudding user response:

This looks like a situation where a configuration file should be used. You can store your "table" in a CSV file or similar (like a database table) and create a class to read from this file and match with the monster you're searching for. Whenever you add new monsters you only need to add them to the configurataion file.

I'm using CsvHelper to read the CSV file below:

public class Monster
{
    public string Gender { get; set; }
    public string Age { get; set; }
    public string Type { get; set; }
    public int Result { get; set; }
}

public class MonsterLookup
{
    private readonly List<Monster> _lookupList;
    
    public MonsterLookup(string configFilePath)
    {
        using var reader = new StreamReader(configFilePath);
        using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);
        _lookupList = csv.GetRecords<Monster>().ToList();
    }
    
    public int GetResult(Monster monster)
    {
        var match = _lookupList.FirstOrDefault(
            m => m.Gender == monster.Gender
                && m.Age == monster.Age
                && m.Type == monster.Type);
        return match?.Result ?? -1;
    }
}

// Call it like:
var monsterLookup = new MonsterLookup(configFilePath);
var result = monsterLookup.GetResult(myMonster);

The config file (you can add all your types):

Gender,Age,Type,Result
Male,Young,Fire,1
Male,Old,Ice,2
Female,Young,Fire,4

See this fiddle for an example.

  • Related