In the DnD world there are a number of things that when expressed in code are traditionally expressed as enumerations. A good example of this is the concept of “size”, which can be one of the following values …
- Tiny
- Small
- Medium
- Large
- Huge
- Gargantuan
In Dart code, the above could be easily expressed as :
enum DnDSize { Tiny, Small, Medium, Large, Huge, Gargantuan };
Unfortunately, Dart enum
is a very limiting concept when it is used to reflect values that are stored in a database or retrieved as a JSON or XML data protocol. The main reason for the limitation is the because it lacks the ability to assign specific values each of the enumerated types.
Take our example above, notice there is no way to specify that DnDSize.Tiny
is equal to the String 'T'
or alternatively the integer value 0
.
A better example would be the concept of “difficulty” in DnD. Consider the following table of challenge levels …
- Very Easy (Difficulty Rating: 5)
- Easy (Difficulty Rating: 10)
- Medium (Difficulty Rating: 15)
- Hard (Difficulty Rating: 20)
- Very Hard (Difficulty Rating: 25)
- Nearly Impossible (Difficulty Rating: 30)
If we tried to express the above as an enumerated type it might look like the following in Dart …
enum DnDChallenge {
VeryEasy,
Easy,
Medium,
Hard,
VeryHard,
NearlyImpossible,
};
However if we expressed it using enum
then we would loose any ability to express the Difficulty Rating values for each. So how do we address this short coming? Well ... we express it as a class
that is specially constructed to provide all the value of an enumerated type, but still retain all the capabilities of a class.
To best understand we will look at the two examples above. First let us create DnDSize
using a class
instead of an enum
.
class DnDSize {
final String _name; const DnDSize._(this._name); @override
String toString() {
return _name;
}
static const DnDSize Tiny = DnDSize._('Tiny');
static const DnDSize Small = DnDSize._('Small');
static const DnDSize Medium = DnDSize._('Medium');
static const DnDSize Large = DnDSize._('Large');
static const DnDSize Huge = DnDSize._('Huge');
static const DnDSize Gargantuan = DnDSize._('Gargantuan');
}
In this example we have created a class DnDSize
that has a private property _name
that can only be set by the constructor which is also private. The only instances of this class that are therefore possible are those created in lines 10 - 15 using the static const
modifiers.
The benefit of this implementation over the enum
equivalent is the addition of a custom toString()
method that we can used to convert the instances of DnDSize
into a string.
NOTE: This is particular useful when you need to convert
DnDSize.Tiny
into a string inside aJSON
message for example.
But we don’t have to stop there … because this is a class, we can also add other methods. In this example it would be very helpful to be able to parse a string into an instance of DnDSize
.
/// Attempts to parse the string representation of
/// a "size" into its corresponding DnDSize instance
/// or return null if unable to parse.
static DnDSize tryParse(String val) {
switch (val.toLowerCase()) {
case 'tiny':
return Tiny;
case 'small':
return Small;
case 'medium':
return Medium;
case 'large':
return Large;
case 'huge':
return Huge;
case 'gargantuan':
return Gargantuan;
default:
print('$val is not a valid size');
return null;
}
}
This approach works just as well for DnDChallenge
as we can see in this next example ...
class DnDChallenge {
final int _value;
const DnDChallenge(this._value); int toInt() {
return _value;
} @override
String toString() {
if (_value <= VeryEasy._value) {
return 'Very Easy ($_value)';
} else if (_value <= Easy._value) {
return 'Easy ($_value)';
} else if (_value <= Medium._value) {
return 'Medium ($_value)';
} else if (_value <= Hard._value) {
return 'Hard ($_value)';
} else if (_value <= VeryHard._value) {
return 'Very Hard ($_value)';
} else {
return 'Nearly Impossible ($_value)';
}
} static const DnDChallenge VeryEasy = DnDChallenge(5);
static const DnDChallenge Easy = DnDChallenge(10);
static const DnDChallenge Medium = DnDChallenge(15);
static const DnDChallenge Hard = DnDChallenge(20);
static const DnDChallenge VeryHard = DnDChallenge(25);
static const DnDChallenge NearlyImpossible = DnDChallenge(30);
}
In this example we are able to create an internal integer _value
for the difficulty rating as also provide a custom toString()
method.
NOTE You may notice in this example that the constructor is not private. This enables the creation of instances of
DnDChallenge
that aren't one of the specific enumerated types. This is optional and is presented here simply to show that it is possible.
Conclusion
Just because Dart’s implementation of enum
is limited doesn't mean you can't still have rich enumerated types. Using the pattern above you can still have code that looks like this ...
switch (size) {
case DnDSize.Tiny:
// ...
case DnDSize.Small:
// ...
default:
// ...
}
But also be able to do things like DnDSize.toString()
and DnDChallenge.toInt()
.
In effect ... you can have your enumerated cake and eat it too!