Designing Entities: Abstract Classes and Enums
Modeling domain entities correctly using abstract base classes and enums. Why enums always beat booleans in production-grade design.
Once you have identified your core classes through visualization, the next challenge is deciding what kind of class each entity should be. An abstract class? A concrete class? An interface? An enum? These decisions shape how flexible your system will be.
The Essentials
- Abstract classes for shared state: If multiple classes (like Human and Bot) share attributes like name and symbol, pull them into an abstract base class.
- Enums beat Booleans: Never use
isBot: boolean. Use aPlayerTypeenum. It is extensible and makes the code self-documenting. - Type Attribute Pattern: Use an enum field in the base class to track the type of the subclass. This avoids the need for
instanceofchecks. - Interfaces for behavior: Use interfaces when multiple classes share a behavior but not necessarily state (e.g.,
BotPlayingStrategy).
Abstract Classes: Handling Shared State
In TicTacToe, we have two types of players: Humans and Bots. Both have a name, a symbol, and a type. Instead of duplicating these attributes, we use an abstract Player class.
abstract class Player {
private name: string;
private symbol: Symbol;
private type: PlayerType;
constructor(name: string, symbol: Symbol, type: PlayerType) {
this.name = name;
this.symbol = symbol;
this.type = type;
}
// Every subclass must implement this
abstract makeMove(board: Board): Cell;
// Getters for shared state
getName(): string { return this.name; }
getSymbol(): Symbol { return this.symbol; }
getType(): PlayerType { return this.type; }
}The abstract class provides a "contract": every player MUST implement makeMove, but they can share the code for getting their name or symbol.
Why Enums Always Beat Booleans
In my early designs, I would use a boolean isBot to distinguish between player types. This is a mistake I see all the time in interviews.
Booleans are binary. If tomorrow you want to add a RemotePlayer or an AIRuntimePlayer, a boolean cannot handle it. You would have to add another boolean like isRemote. Now you have four possible combinations of booleans, some of which (like isBot && isRemote) might not even make sense.
An enum handles this transition perfectly:
enum PlayerType {
HUMAN,
BOT,
REMOTE
}The code becomes more readable and future-proof. if (player.getType() == PlayerType.BOT) is much clearer than if (player.isBot()).
The Type Attribute Pattern
Notice that in the Player class above, we store a type attribute of type PlayerType. This is the Type Attribute Pattern.
By storing the type in the base class, we can check a player's type without ever using the instanceof operator or casting. instanceof is generally avoided in clean LLD because it couples your code to specific subclass implementations. Storing the type as an enum keeps the logic clean and centralized.
When to Use Interfaces
I use interfaces when I want to define a behavior that different classes can "plug into," regardless of their hierarchy.
In TicTacToe, a Bot doesn't know how it plays; it delegates its move logic to a BotPlayingStrategy. Since different bots might use different algorithms (Easy, Hard, Alpha-Beta), we define BotPlayingStrategy as an interface. This allows us to swap the algorithm at runtime without changing the Bot class.
Choosing the right structure for your entities is the foundation of a good design. In the next post, we will look at how these entities relate to each other through aggregation and composition.
Further Reading and Watching
Practice what you just read.
Keep reading