What table-driven design means
Table-driven design replaces sprawling conditionals with structured data. Whenever branching logic keeps growing, move the rules into a table and let the program look up the answer. The approach shines in state machines, permission matrices, pricing rules, and plenty of day-to-day backend tasks.
Here are three lightweight examples that demonstrate the idea.
Example 1: Rock, paper, scissors
Instead of a long if/else chain, keep a victory map and let a lookup drive the full round logic:
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>
enum Move {
MOVE_INVALID = 0,
MOVE_ROCK = 1,
MOVE_SCISSORS= 2,
MOVE_PAPER = 3
};
static const enum Move WINNING_MOVES[] = {
MOVE_INVALID,
MOVE_SCISSORS, // ROCK beats SCISSORS
MOVE_PAPER, // SCISSORS beats PAPER
MOVE_ROCK // PAPER beats ROCK
};
static const char *MOVE_LABELS[] = {
"invalid",
"rock",
"scissors",
"paper"
};
static enum Move random_move(void) {
return (enum Move)(rand() % 3 + 1);
}
static bool player_wins(enum Move player, enum Move opponent) {
if (player == opponent) {
return false; // draw
}
return WINNING_MOVES[player] == opponent;
}
int main(void) {
srand((unsigned)time(NULL));
printf("Choose 1(rock) / 2(scissors) / 3(paper):\n");
int number = 0;
if (scanf("%d", &number) != 1 || number < MOVE_ROCK || number > MOVE_PAPER) {
printf("Invalid input.\n");
return 1;
}
enum Move player = (enum Move)number;
enum Move computer = random_move();
printf("Player: %s\n", MOVE_LABELS[player]);
printf("Computer: %s\n", MOVE_LABELS[computer]);
if (player == computer) {
printf("Result: draw\n");
} else if (player_wins(player, computer)) {
printf("Result: player wins\n");
} else {
printf("Result: computer wins\n");
}
return 0;
}
If new gestures join the game (e.g., “lizard, Spock”), extend the enum and the lookup table and the rest of the program remains untouched.
Example 2: Grade conversion
Mapping scores to letter grades becomes trivial:
char make_grade(uint32_t score) {
if (score > 100) {
return 'X';
}
static const char GRADES[] = {
'F','F','F','F','F','F','D','C','B','A','A'
};
return GRADES[score / 10];
}
One glance at the array tells you exactly how scores translate to letters.
Example 3: Days per month
Combine leap-year logic with two static arrays:
uint32_t days_in_month(uint32_t year, uint32_t month) {
static const uint32_t DAYS_COMMON_YEAR[] = {
31,28,31,30,31,30,31,31,30,31,30,31
};
static const uint32_t DAYS_LEAP_YEAR[] = {
31,29,31,30,31,30,31,31,30,31,30,31
};
bool leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
return leap ? DAYS_LEAP_YEAR[month - 1] : DAYS_COMMON_YEAR[month - 1];
}
Now the month logic lives in data, not scattered across conditionals.
Practical lessons
- Draw the table first: enumerate inputs, outputs, and edge cases before coding.
- Separate responsibilities: keep lookup logic in one place and let data owners manage the table content.
- Watch performance: cache large tables or index them to avoid repeated scans.
- Let non-engineers help: store tables in config files or databases so operations teams can adjust rules safely.
Where it shines
- Combinatorial conditions turning into an if/else explosion.
- Stable logic with frequently updated rule content.
- Systems where business users own the rules.
Final thoughts
Table-driven design is simple yet powerful. By moving decisions into data, you keep code shorter, easier to test, and friendlier for future maintainers.