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.

April 27, 20263 min read2 / 9

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

  1. 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.
  2. Enums beat Booleans: Never use isBot: boolean. Use a PlayerType enum. It is extensible and makes the code self-documenting.
  3. Type Attribute Pattern: Use an enum field in the base class to track the type of the subclass. This avoids the need for instanceof checks.
  4. 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.

Enum vs Boolean Challenge
1 exercise