I firmly believe this is the number-one rule for clear code.
I'm becoming allergic to code like this:
# BAD EXAMPLE, DO NOT USE
# Every entry has a key.
# Every entry is either a tombstone or a non-tombstone that carries a value.
class Entry:
key: str
value: str
tombstone: bool
The problem is that there are illegal states that can be represented with the Entry datatype:
# BAD EXAMPLE, DO NOT USE
# What does this even mean? Is it a tombstone or not??
e = Entry(
key="abc",
value="xyz",
tombstone=True)
A good guiding principle to avoid this kind of confusion is to make illegal states unrepresentable:
class ValueEntry:
key: str
value: str
class TombstoneEntry:
key: str
Entry = ValueEntry | TombstoneEntry
Now it is obvious from the types alone that (1) tombstones do not carry a value and (2) anything with a value cannot be a tombstone.
You can apply this principle in Python (as above) or Rust:
enum Entry {
Value { key: String, value: String },
Tombstone { key: String },
}
or even in modern Java:
public sealed interface Entry permits Entry.Value, Entry.Tombstone {
public static record Value(String key, String value) {}
public static record Tombstone(String key) {}
}
A closely related principle is to make redundant states share one representation. In particular, long lists of boolean flags should be highly suspect:
# BAD EXAMPLE, DO NOT USE
class SystemState:
active: bool
corrupt: bool # NOTE: ignored when not active
# These two SystemStates are logically equivalent
a = SystemState(active=False, corrupt=False)
b = SystemState(active=False, corrupt=True)
Instead, only the relevant states should be representable:
class SystemState(Enum):
ACTIVE = 1
INACTIVE = 2
CORRUPT = 3
Too often I've seen documentation used as a crutch for these principles. One can get away with myriad sins by carefully documenting the corner cases and the meaning of each data type and field. On the other hand, following these principles can save you a lot of documentation effort by making the types themselves document which states are legal.
These principles are closely related to the pattern of using dataflow and types for preconditions: having a named type for, say, UpperCaseString makes the legal representations obvious in code.