8.9 KiB
8.9 KiB
title |
---|
The Humble Enumeration |
What in the heck is an enum?
enum MentalState {
WAIT,
OH_GOD,
WHAT
}
And why?
enum UsefulFor {
EVERYTHING,
A_FEW_THINGS,
NOTHING_AT_ALL
}
What in the heck is an enum?
enum MentalState {
WAIT,
OH_GOD,
WHAT
}
And why?
enum UsefulFor {
EVERYTHING, // ???
A_FEW_THINGS, // ???
NOTHING_AT_ALL // ???
}
Enumerated types
An enum is essentially a known set of distinct values.
A couple of common enums would be things like:
- Playing card suits (HEART, SPADE, CLUB, DIAMOND)
- Days of the week (SUNDAY, MONDAY, etc.)
- Log levels (DEBUG, INFO, WARNING, ERROR, FATAL)
- Etc.
Okay, that's great, but how do I know when to use one?
(Or, "Booleans? More like, Fooleans!")
So, picture this. You've got some code that analyzes a computer network.
There is a lovely Computer class:
public class Computer {
String hostname;
double maxPowerConsumption;
boolean on;
}
And a lovely function that calculates how much power a collection of computers might use:
public double estimateWattage(List<Computer> computers) {
double sum = 0;
for (var computer : computers) {
if (computer.on) {
sum += computer.maxPowerConsumption;
}
}
return sum;
}
This works great.
Until someone says...
"What about computers in standby?"
Oh.
True.
Well, okay, maybe let's add a new inStandby field to Computer?
public class Computer {
String hostname;
double maxPowerConsumption;
boolean on;
boolean inStandby;
}
and update our wattage function:
public double estimateWattage(List<Computer> computers) {
double sum = 0;
for (var computer : computers) {
if (computer.on) {
sum += computer.maxPowerConsumption;
} else if (computer.inStandby) {
sum += computer.maxPowerConsumption * 0.01;
}
}
return sum;
}
That works.
Probably.
But it's starting to look a little ugly.
And, wait. Is a computer in standby... powered on? Or is it off?
Hmm...
No matter how you answer the question, you've got yourself a problem.
There is now an invalid state that an instance of Computer could end up in.
Let's say we decide that a computer inStandby is on.
That means that having an instance where .inStandby == true
and .on == false
is illegal.
And every part of your code better understand that, because the compiler does not.
The technical term for this is,
"Yucky"
or
"Ew"
Hmmmmmm....
Well. We could try a String.
public class Computer {
String hostname;
double maxPowerConsumption;
String powerState;
}
But then, how do we know if Computer's powerState is valid?
Are we going to enjoy writing code like this?
public static final String IN_STANDBY = "IN_STANDBY";
if (computer.powerState.toUpperCase().equals(IN_STANDBY)) {
sum += computer.maxPowerConsumption * 0.01;
}
My guess is, not so much.
Instead,
What if we try an enum?
enum PowerState {
POWERED_ON,
STANDBY,
POWERED_OFF
}
Enums can describe more than two distinct states.
But they're a lot more strict than a String.
And we can use that inside our Computer class.
public class Computer {
String hostname;
double maxPowerConsumption;
PowerState powerState;
}
Then estimateWattage() cleans up pretty nicely!
public double estimateWattage(List<Computer> computers) {
double sum = 0;
for (var computer : computers) {
switch (computer.powerState) {
case POWERED_ON:
sum += computer.maxPowerConsumption;
break;
case STANDBY:
sum += computer.maxPowerConsumption * 0.01;
break;
case POWERED_OFF:
// Do nothing
break;
}
}
return sum;
}
And adding a new power state has become a lot easier:
switch (computer.powerState) {
case POWERED_ON:
sum += computer.maxPowerConsumption;
break;
case IDLE:
sum += computer.maxPowerConsumption * 0.12;
break;
case STANDBY:
sum += computer.maxPowerConsumption * 0.01;
break;
case POWERED_OFF:
// Do nothing
break;
}
This is pretty much the extent of enum capabilites in C, C++, and C#
But now this is starting to look repetitive.
So, what if we do some Java?
Could we store that multiplier on the enum itself?
enum PowerState {
POWERED_ON(1.0),
IDLE(0.12),
STANDBY(0.01),
POWERED_OFF(0);
public final double wattageMultiplier;
PowerState(double mult) {
this.wattageMultiplier = mult;
}
}
Hey, we can!
Java lets us associate any arbitrary data with an enum.
Though it's good practice to stick with something immutable.
(Also: note the semicolon after the last value. It was optional before!)
Now, estimateWattage() doesn't need to know what the multipliers are!
public double estimateWattage(List<Computer> computers) {
double sum = 0;
for (var computer : computers) {
var power = computer.powerState;
sum += computer.maxPowerConsumption * power.wattageMultiplier;
}
return sum;
}
It just needs to know that a multiplier exists.
And, what the heck, let's use streams.
public double estimateWattage(List<Computer> computers) {
return computers.stream()
.mapToDouble(computer ->
computer.maxPowerConsumption *
computer.powerState.wattageMultiplier)
.sum();
}
Functional!
If you've futzed with streams a lot, you may notice an opportunity here.
If Computer had some sort of method for:
computer.maxPowerConsumption *
computer.powerState.wattageMultiplier
We could use that directly in mapToDouble(), via a method reference
So, let's define that method.
public class Computer {
// snip
public double currentPowerConsumption() {
return maxPowerConsumption * powerState.wattageMultiplier;
}
}
Simple enough.
Cool.
And that leaves us with this fairly-neat implementation:
public double estimateWattage(List<Computer> computers) {
return computers.stream()
.mapToDouble(Computer::currentPowerConsumption)
.sum();
}
It almost doesn't even need a dedicated method anymore.
But I'll leave that as an exercise for the reader.
Side note
In Java, you can also define methods directly on an enum.
For example, say we still wanted a way to check if a computer is on or off.
We could add a method directly to PowerState:
enum PowerState {
POWERED_ON(1.0),
IDLE(0.12),
STANDBY(0.01),
POWERED_OFF(0);
// snip
public boolean isOn() {
return this != POWERED_OFF;
}
}
And that keeps the logic all in one place.
In summary,
Wait, when should you use enums?
Basically any time you have a known set of distinct states.
When should you definitely not use an enum?
Things like:
- Names
- Numbers
- IPs
- Prices
- Etc.
Because they aren't known beforehand, and there are too many to enumerate yourself.