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