Enums versus class hierarchies
A fixed list of kinds, or a family of classes that behave differently.
01The intuition
The problem it solves
A canteen token machine sells three kinds of token: tea, snack and lunch. The first version stores the kind as a string and checks it wherever it matters.
def price(kind: str) -> int:
if kind == "tea": return 10
if kind == "snack": return 25
if kind == "lunch": return 60
raise ValueError(kind)
def can_buy_before_noon(kind: str) -> bool:
return kind in ("tea", "snack")
price("Tea") # ValueError at runtime, a capital letter
price("lunh") # ValueError, a typo nobody caught
Strings let anything through. A typo is a runtime crash. The list of valid kinds is written in two places and will drift. On the other hand, making a full class per token kind, with TeaToken, SnackToken and LunchToken subclasses, feels like too much machinery for three prices.
The decision is one you make in nearly every design interview: is this a fixed list of named options, which wants an enum, or a family of things that behave differently, which wants a class hierarchy? Choosing wrong either buries behaviour in switch statements or inflates three constants into three files.
Build it one piece at a time
Step 1: make the kinds a closed, spelled-once list. An enum is a type with exactly these members, so a typo is an error the moment the code is read, not when a customer presses a button.
from enum import Enum
class TokenKind(Enum):
TEA = "tea"
SNACK = "snack"
LUNCH = "lunch"
TokenKind("tea") # TokenKind.TEA
TokenKind("lunh") # ValueError, but now at the single parsing boundary
Step 2: attach the data each kind carries. Price and the morning rule are facts about a kind, so they live on the member instead of in a function far away.
from enum import Enum
class TokenKind(Enum):
TEA = (10, True)
SNACK = (25, True)
LUNCH = (60, False)
def __init__(self, price: int, before_noon: bool):
self.price = price
self.before_noon = before_noon
TokenKind.LUNCH.price # 60
TokenKind.TEA.before_noon # True
Step 3: notice when behaviour starts to differ. Suppose a lunch token now needs a different validation: it must be bought for a specific day and expires at 3 pm, while tea tokens never expire. That is no longer a number per kind; it is different code per kind. Keep the enum for the name, and move the behaviour into a small hierarchy behind one interface.
from abc import ABC, abstractmethod
from datetime import datetime, time
class TokenRule(ABC):
@abstractmethod
def valid_at(self, moment: datetime) -> bool: ...
class AlwaysValid(TokenRule):
def valid_at(self, moment: datetime) -> bool:
return True
class ExpiresAt(TokenRule):
def __init__(self, deadline: time): self.deadline = deadline
def valid_at(self, moment: datetime) -> bool:
return moment.time() <= self.deadline
RULES = {
TokenKind.TEA: AlwaysValid(),
TokenKind.SNACK: AlwaysValid(),
TokenKind.LUNCH: ExpiresAt(time(15, 0)),
}
Step 4: keep the two tools in their lanes. The enum answers "which one" and carries plain facts; the rule objects answer "how does it behave". A new kind with an existing behaviour is one enum line; a new behaviour is one small class.
| Situation | Right tool | Adding a member costs | Adding a behaviour costs |
|---|---|---|---|
| Fixed names with a few facts each | enum with fields | one line | a branch everywhere |
| Kinds with different code paths | interface plus classes | one class | one method on each class |
| Names plus a few differing behaviours | enum plus a map to rule objects | one line | one class, one map entry |
Why it works
An enum makes the set of possibilities a fact the compiler and the reader both know, so every switch over it can be checked for completeness and every value is spelled once. Its invariant is "no value outside this list exists". A hierarchy makes behaviour live with the thing that owns it, which is polymorphism; its invariant is "every member answers the shared interface". The failure mode of the enum is the growing switch: once three functions each branch on the kind, you are maintaining a hierarchy by hand. The failure mode of the hierarchy is the empty class: subclasses that differ only by a constant should have been data.
The trade-off you must say out loud
Say the test you applied: "do the kinds differ in data or in code". Then say what changes when the interviewer adds a kind, because they will. An enum keeps related facts on one screen and is easy to serialise and store; a hierarchy keeps related code together and is easy to extend without editing. The hybrid, an enum that maps to behaviour objects, is often the honest answer for a parking lot or a vending machine, and naming it as a deliberate choice earns more than picking either extreme.
Where it shows up in the 100 Google problems
- Design a parking lot:
VehicleSizeandSpotSizeare enums; fits-or-not is a comparison of data, not a subclass per vehicle. - Design a vending machine: coin denominations are an enum with a value; the machine's modes are a hierarchy of state objects because each mode handles the same buttons differently.
- Design a deck of cards and Blackjack:
SuitandRankare enums carrying the card's point value; the dealer's behaviour is a rule class. - Design a JSON parser: token types are an enum for the tokenizer; parsed values are a small hierarchy because objects, arrays and scalars serialise differently.