Search code examples
c#if-statementdata-structuresdesign-patternsnested-if

How to restructure nested if else statement


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?


Solution

  • 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).