Parte III · Diseñar bien
Capítulo 7 — Familias de cartas (jerarquías)
En el capítulo anterior diste un paso atrás y dibujaste el plano de Kriaturas.
Quedó claro y limpio: jugadores, cartas, mazos y las líneas que los conectan. Pero
ese plano escondía una mentira piadosa. Mira la tabla carta que vienes
arrastrando desde el capítulo 2:
| id | nombre | rareza | coste |
|---|---|---|---|
| 1 | Chispín | común | 2 |
| 2 | Flamita | común | 2 |
| 5 | Entreno intensivo | común | 1 |
| 6 | Poción | común | 1 |
A primera vista, cuatro cartas iguales. Pero no lo son, y tú lo sabes. Chispín es una criatura: ataca, se defiende, tiene vida y pertenece a un elemento. Entreno intensivo no ataca a nadie: es una carta de entrenamiento que aplica un efecto durante unos turnos. Poción es un objeto que se usa y se gasta. Tres cosas muy distintas… metidas en la misma caja, como si fueran lo mismo.
Hasta ahora lo dejamos pasar a propósito, para no complicarte la vida. Pero el juego no avanza si una criatura no sabe cuánta vida tiene. Ha llegado el momento de modelar que estas cartas son parientes pero distintas. De eso va este capítulo.
Al acabar sabrás reconocer cuándo varias entidades forman una familia, qué es una jerarquía y, sobre todo, las tres formas de llevarla a tablas y cómo elegir entre ellas con criterio.
Cosas que se parecen, pero no son iguales
Empecemos por la idea, sin tablas todavía. Piensa en las cartas de Kriaturas y hazte dos preguntas.
¿Qué tienen en común todas las cartas? Todas tienen un nombre, una rareza y un coste para jugarlas. Da igual que sea una criatura o una poción: esos tres datos los tiene cualquier carta.
¿En qué se diferencian? Aquí cada tipo va por su lado:
- Una criatura tiene
elemento(fuego, agua, planta, eléctrico…),ataque,defensayvida. - Un entrenamiento tiene un
efectoy unaduracion(cuántos turnos dura). - Un objeto tiene un
efectoy un dato de si esconsumible(si se gasta al usarlo o no).
Cuando varias entidades comparten una parte común pero cada una tiene rasgos propios que te interesa guardar, tienes una jerarquía: una familia con un tipo general arriba y tipos específicos debajo.
Al tipo general se le llama superclase. En nuestro caso es carta, con lo
que tienen todas: nombre, rareza, coste. A los tipos específicos se les llama
subclases: criatura, entrenamiento y objeto, cada uno con sus atributos
propios. Y la idea que lo une todo es la herencia: cada subclase "hereda" los
atributos comunes de la superclase y les añade los suyos. Una criatura es una
carta (tiene nombre, rareza y coste) y además tiene ataque, defensa y vida.
A esto, en los libros, lo llaman generalización/especialización, según desde dónde lo mires. Si subes de las criaturas, entrenamientos y objetos hacia el concepto común "carta", estás generalizando. Si bajas de "carta" hacia cada tipo concreto, estás especializando. Es el mismo dibujo leído en dos sentidos; no te agobies con los dos nombres, describen la misma familia.
Antes de dibujarla, dos matices rápidos que conviene tener en el radar, sin liarse con ellos:
- La especialización puede ser total (toda carta es de uno de los tres tipos, no hay cartas "sueltas") o parcial (podría haber cartas que no caen en ninguna subclase).
- Las subclases pueden ser disjuntas (una carta es de un solo tipo) o solapadas (una misma carta podría ser de varios).
En Kriaturas lo tenemos fácil: total y disjunta. Cada carta es criatura o entrenamiento o objeto, una y solo una. Lo decimos y seguimos; esto basta para casi todo.
El plano de la familia
Como en el capítulo anterior, dibujemos la jerarquía antes de tocar tablas. Primero en cajas y líneas, que se lee en cualquier sitio:
┌───────────────────────┐
│ carta │ (superclase)
│ id, nombre, rareza, │
│ coste │
└───────────┬───────────┘
│ es un / es una
┌──────────────┼──────────────┐
│ │ │
┌──────────┴───────┐ ┌────┴──────────┐ ┌─┴────────────────┐
│ criatura │ │ entrenamiento │ │ objeto │
│ elemento, ataque │ │ efecto, │ │ efecto, │
│ defensa, vida │ │ duracion │ │ consumible │
└──────────────────┘ └───────────────┘ └──────────────────┘
La línea que baja de carta a sus tres tipos es la herencia: se lee "una
criatura es una carta", "un objeto es una carta". Y la misma idea en
Mermaid, que las herramientas web renderizan como dibujo:
Fíjate en que aquí usamos un diagrama de clases (la flecha hueca ◁ significa "hereda de"), no el
erDiagramde entidades del capítulo 6. Es a propósito: una jerarquía no es una relación normal entre dos entidades, es una familia. El dibujo de clases lo deja más claro y, de paso, es el mismo que verás cuando programes con clases que heredan.
Tienes el plano. Pero un plano no se guarda: las tablas sí. Y aquí viene lo bueno, porque esta familia se puede convertir en tablas de tres maneras distintas.
De la jerarquía a las tablas: tres caminos
No hay una única forma "correcta" de guardar una jerarquía. Hay tres clásicas, y cada una tiene su precio. Vamos con las tres usando las mismas cartas de siempre, para que veas la diferencia con datos reales.
Camino 1: una sola tabla para toda la familia
La idea más directa: una única tabla carta con todas las columnas posibles,
las comunes y las de cada subclase juntas. Añadimos una columna tipo para saber
qué es cada fila.
CREATE TABLE carta (
id INTEGER PRIMARY KEY,
nombre VARCHAR(40),
rareza VARCHAR(15),
coste INTEGER,
tipo VARCHAR(15), -- 'criatura', 'entrenamiento' u 'objeto'
-- atributos de criatura:
elemento VARCHAR(15),
ataque INTEGER,
defensa INTEGER,
vida INTEGER,
-- atributos de entrenamiento y objeto:
efecto VARCHAR(100),
duracion INTEGER,
consumible BOOLEAN
);
Metemos las cartas de siempre y mira lo que pasa:
| id | nombre | tipo | elemento | ataque | defensa | vida | efecto | duracion | consumible |
|---|---|---|---|---|---|---|---|---|---|
| 1 | Chispín | criatura | eléctrico | 30 | 20 | 50 | — | — | — |
| 5 | Entreno intensivo | entrenamiento | — | — | — | — | +10 ataque | 2 | — |
| 6 | Poción | objeto | — | — | — | — | cura 20 vida | — | sí |
¿Ves el problema? La tabla está llena de huecos. Chispín no usa efecto,
duracion ni consumible. La Poción no usa elemento, ataque, defensa ni
vida. Cada fila deja vacías las columnas que no le tocan.
A cambio, es la más sencilla de todas: una tabla, una consulta, nada de juntar nada. Para una familia con pocos atributos propios puede valer perfectamente. Para una con muchos, acabas con una tabla ancha y agujereada.
Camino 2: una tabla por subclase
El extremo opuesto: ninguna tabla carta, sino una tabla por cada tipo. Cada una
con sus atributos propios y una copia de los comunes (nombre, rareza, coste).
CREATE TABLE criatura (
id INTEGER PRIMARY KEY,
nombre VARCHAR(40),
rareza VARCHAR(15),
coste INTEGER,
elemento VARCHAR(15),
ataque INTEGER,
defensa INTEGER,
vida INTEGER
);
CREATE TABLE entrenamiento (
id INTEGER PRIMARY KEY,
nombre VARCHAR(40),
rareza VARCHAR(15),
coste INTEGER,
efecto VARCHAR(100),
duracion INTEGER
);
CREATE TABLE objeto (
id INTEGER PRIMARY KEY,
nombre VARCHAR(40),
rareza VARCHAR(15),
coste INTEGER,
efecto VARCHAR(100),
consumible BOOLEAN
);
Ahora no hay ni un solo hueco: cada tabla tiene justo las columnas que necesita.
Pero aparece otro problema. Los atributos comunes (nombre, rareza, coste)
están repetidos en las tres tablas. Y, peor aún, las cartas ya no viven en un
único sitio: están repartidas en tres tablas distintas.
Eso complica las preguntas que cruzan tipos. ¿Quieres listar todas las cartas de rareza "épica", sean del tipo que sean? Con este camino tienes que mirar en las tres tablas y unir los resultados a mano. Recuerda lo que aprendiste en el capítulo 5: cuando un dato común se repite en varios sitios, tarde o temprano acaba descuadrado. Esto huele a eso.
Camino 3: una tabla por entidad (la superclase y cada subclase)
El término medio, y el que vamos a adoptar como oficial del libro. Mantienes
la tabla carta con lo común, y creas una tabla por subclase solo con los
atributos propios de cada una. ¿Cómo se conectan? Con un viejo conocido del
capítulo 4: la clave foránea.
La gracia está en un detalle: el id de cada tabla de subclase es, a la vez, su
clave primaria y una clave foránea que apunta al id de carta. Es decir,
una criatura y la carta de la que procede comparten el mismo id. No es un
número nuevo: es el mismo de su carta.
CREATE TABLE carta (
id INTEGER PRIMARY KEY,
nombre VARCHAR(40),
rareza VARCHAR(15),
coste INTEGER,
tipo VARCHAR(15)
);
CREATE TABLE criatura (
id INTEGER PRIMARY KEY,
elemento VARCHAR(15),
ataque INTEGER,
defensa INTEGER,
vida INTEGER,
FOREIGN KEY (id) REFERENCES carta(id)
);
CREATE TABLE entrenamiento (
id INTEGER PRIMARY KEY,
efecto VARCHAR(100),
duracion INTEGER,
FOREIGN KEY (id) REFERENCES carta(id)
);
CREATE TABLE objeto (
id INTEGER PRIMARY KEY,
efecto VARCHAR(100),
consumible BOOLEAN,
FOREIGN KEY (id) REFERENCES carta(id)
);
Lee con calma la tabla criatura. Su id es la clave primaria (no se repite) y a
la vez una clave foránea hacia carta (tiene que existir esa carta). Lo común se
guarda una sola vez, en carta. Lo propio de cada tipo, en su tabla. Cero huecos
y cero repeticiones.
Para saber más: el tipo
BOOLEAN. Hemos puestoconsumible BOOLEAN, un tipo pensado para guardar solo dos valores: verdadero o falso (sí o no). Es el más legible para una columna de "sí/no" como esta. Un detalle por si lo ves en otros sitios: no todos los motores de base de datos lo tratan igual. La mayoría lo aceptan tal cual, pero algunos no tienenBOOLEANde serie y por dentro lo guardan como un número (0 = falso, 1 = verdadero). El concepto es idéntico; solo cambia el envoltorio. Si algún día tu motor se queja deBOOLEAN, ya sabes por dónde van los tiros.
Así quedan los datos. Primero, lo común en carta:
| id | nombre | rareza | coste | tipo |
|---|---|---|---|---|
| 1 | Chispín | común | 2 | criatura |
| 5 | Entreno intensivo | común | 1 | entrenamiento |
| 6 | Poción | común | 1 | objeto |
Y lo propio, cada cosa en su sitio:
criatura entrenamiento objeto ┌────┬──────────┐ ┌────┬──────────┐ ┌────┬─────────────┬──────────┐ │ id │ ataque… │ │ id │ duracion │ │ id │ efecto │ consumible│ ├────┼──────────┤ ├────┼──────────┤ ├────┼─────────────┼──────────┤ │ 1 │ 30… │ │ 5 │ 2 │ │ 6 │ cura 20 vida│ sí │ └────┴──────────┘ └────┴──────────┘ └────┴─────────────┴──────────┘ id=1 apunta id=5 apunta id=6 apunta a carta(1) a carta(5) a carta(6)
Fíjate en que la criatura de la tabla criatura tiene id = 1, el mismo que
Chispín en carta. Para reconstruir la criatura completa, juntas las dos
tablas por ese id con un JOIN, exactamente la herramienta que aprendiste en el
capítulo 5:
SELECT c.nombre, c.rareza, cr.elemento, cr.ataque, cr.vida
FROM carta c
JOIN criatura cr ON cr.id = c.id
WHERE c.tipo = 'criatura';
Resultado:
| nombre | rareza | elemento | ataque | vida |
|---|---|---|---|---|
| Chispín | común | eléctrico | 30 | 50 |
Lo común sale de carta, lo propio de criatura, unidos por el id compartido.
Es la opción más ordenada, la que mejor refleja el plano que dibujaste, y la que
seguiremos usando en el resto del libro.
Entonces, ¿cuál elijo?
No hay ganador absoluto, y esto es importante. Cada camino cambia algo:
- Camino 1 (una tabla): lo más simple de consultar, pero con huecos. Bien si los tipos tienen pocos atributos propios.
- Camino 2 (tabla por subclase): sin huecos, pero repite lo común y dispersa las cartas. Bien si casi nunca mezclas tipos en una consulta.
- Camino 3 (tabla por entidad): lo más limpio y normalizado, a cambio de un
JOINpara recomponer cada carta. El que mejor escala cuando la familia crece.
Lo importante no es memorizar cuál es "el bueno", sino saber que existen los tres y poder justificar tu elección según tu caso: cuántos atributos propios hay, cuántos huecos te molestan, qué preguntas vas a hacer más. Eso es diseñar con criterio. Y es justo lo que te separa de quien deja que la herramienta decida sin saber qué eligió por él.
Resumen
Has aprendido a modelar familias de cartas. Cuando varias entidades comparten una
parte y difieren en otra, tienes una jerarquía: una superclase con lo
común y subclases que heredan eso y añaden lo suyo. En Kriaturas, carta
es la superclase, y criatura, entrenamiento y objeto las subclases.
Y has visto que esa jerarquía se lleva a tablas