Las clases del nivel 2 te dejaron modelar objetos con soltura. Este capítulo baja un piso más, al
modelo de objetos que hay debajo: claves que nunca colisionan (Symbol), colecciones que no
retienen memoria (WeakMap y WeakSet), propiedades que se calculan solas
(getters/setters) y la capacidad de interceptar lo que se hace con un objeto (Proxy y
Reflect). Es la maquinaria que usan por dentro librerías que ya conoces de oídas; verla te quita
la sensación de magia.
Seguimos con el Team Builder. El hilo es una ficha de héroe: un winrate que siempre cuadra y unas notas internas del scout que no deben filtrarse al backend.
Symbol: una clave que nunca colisiona#
Un Symbol es un valor primitivo único: cada vez que llamas a Symbol() obtienes uno
irrepetible, aunque le pongas la misma descripción. Esa descripción es solo una etiqueta para depurar;
no afecta a la identidad.
// un símbolo nuevo
const a = Symbol("rol");
// otro símbolo, con la MISMA descripción
const b = Symbol("rol");
// false: dos símbolos nunca son iguales entre sí
console.log(a === b);¿Para qué sirve algo que nunca es igual a otra cosa? Para usarlo como clave de objeto que no choca
con ninguna otra. Si dos librerías guardan datos en tu objeto con la clave "id", se pisan; con un
Symbol, cada una tiene su clave única. Y hay un bonus: las claves de tipo Symbol quedan fuera
de Object.keys y de JSON.stringify, así que son perfectas para metadatos ocultos:
// nuestra clave para datos internos
const INTERNO = Symbol("interno");
const heroe = { nombre: "Tracer", rol: "Daño" };
// guardamos metadatos bajo la clave simbólica
heroe[INTERNO] = { revisadoPor: "scout-3" };
// ['nombre', 'rol'] -> la clave Symbol no se enumera
console.log(Object.keys(heroe));
// {"nombre":"Tracer","rol":"Daño"} -> tampoco se serializa
console.log(JSON.stringify(heroe));
// { revisadoPor: 'scout-3' } -> pero sigue ahí, si tienes el símbolo
console.log(heroe[INTERNO]);
// `Object.getOwnPropertySymbols` devuelve un array con las claves de tipo Symbol
// de un objeto, en orden de creación: las recupera aunque no aparezcan en Object.keys.
// [Symbol(interno)] -> recuperable: oculto no es privado
console.log(Object.getOwnPropertySymbols(heroe));Un matiz que conviene fijar: quedar fuera de Object.keys y JSON.stringify hace la clave
oculta a la enumeración, no privada. Quien tenga el símbolo —o lo rescate con
Object.getOwnPropertySymbols— puede leerla. Para datos que de verdad no deberían vivir dentro del
objeto, el WeakMap de la siguiente sección es la opción limpia.
En el capítulo anterior ya usaste uno sin saber su nombre: Symbol.iteratorfor...of sabe recorrerlo.
WeakMap y WeakSet: datos atados a un objeto, sin fugas#
Ya conoces Map y Set del nivel 2. Sus primos débiles se parecen, con dos diferencias que los
hacen ideales para asociar datos a objetos sin estorbar:
Un WeakMap es como un Map, pero sus claves solo pueden ser objetos y las guarda con una
referencia débil: si ese objeto deja de usarse en el resto del programa, su entrada en el WeakMap
se libera sola. Sirve para guardar datos privados asociados a un objeto sin mantenerlo vivo a la
fuerza:
// las claves serán objetos héroe
const notas = new WeakMap();
const tracer = { nombre: "Tracer" };
// asociamos una nota a ESE objeto
notas.set(tracer, "Agresiva; brilla en control");
// 'Agresiva; brilla en control'
console.log(notas.get(tracer));
// La nota no es una propiedad de tracer: vive fuera. Si tracer dejara de usarse,
// su entrada en el WeakMap se descartaría sin que tú hagas nada (cero fugas).Un WeakSet es el equivalente de Set: solo guarda objetos, con referencias débiles. Su uso
típico es marcar objetos sin alterarlos —“este héroe ya pasó la validación”, “este nodo ya se
procesó”— sin impedir que se liberen luego:
// un conjunto de objetos "ya validados"
const validados = new WeakSet();
const ana = { nombre: "Ana" };
// marcamos a ana como validada
validados.add(ana);
// true: ¿está marcada? sí
console.log(validados.has(ana));
// No hemos tocado el objeto ana: la marca vive en el WeakSet, aparte.Lo “débil” es la clave: no son para recorrer (no se pueden iterar) ni para listar su contenido; son para asociar y consultar datos por objeto sin crear fugas de memoria.
El otro uso que verás todo el rato es una caché por objeto: guardar el resultado de un cálculo caro la primera vez y reutilizarlo después, sin ensuciar el objeto ni arriesgar una fuga.
// asocia cada héroe con un resultado ya calculado
const cache = new WeakMap();
function statsCaras(heroe) {
// ya calculado antes: lo reutilizamos
if (cache.has(heroe)) return cache.get(heroe);
// imagina aquí un cálculo costoso
const resultado = heroe.victorias / heroe.partidas;
// lo guardamos asociado a ESTE héroe, para la próxima
cache.set(heroe, resultado);
return resultado;
}
const tracer = { nombre: "Tracer", victorias: 78, partidas: 120 };
// 0.65 -> primera vez: lo calcula y lo cachea
console.log(statsCaras(tracer));
// 0.65 -> segunda vez: lo saca de la caché, sin recalcular
console.log(statsCaras(tracer));
// Si 'tracer' deja de usarse en el programa, su entrada en la caché se libera sola.Getters y setters: propiedades que se calculan solas#
Un getter (get) es un método que se lee como si fuera una propiedad. No guarda un valor: lo
calcula cada vez que se lee. Eso lo hace perfecto para datos derivados como el winrate, que nunca
debe quedarse desfasado:
const heroe = {
victorias: 78,
partidas: 120,
get winrate() {
// se ejecuta al leer heroe.winrate, no al definirlo
// siempre desde los datos actuales
return this.victorias / this.partidas;
},
};
// 0.65 -> se lee SIN paréntesis, como una propiedad
console.log(heroe.winrate);
// cambiamos el dato base
heroe.victorias = 90;
heroe.partidas = 130;
// 0.692… -> recalculado solo, sin tocar nada más
console.log(heroe.winrate);Su pareja, el setter (set), corre cuando asignas a la propiedad. Sirve para validar o
transformar antes de guardar:
const heroe = {
// el valor real; el guion bajo es una convención de "no me toques directo"
_rango: "Oro",
get rango() {
// leer heroe.rango devuelve el guardado
return this._rango;
},
set rango(nuevo) {
// corre al hacer heroe.rango = '...'
const validos = ["Bronce", "Plata", "Oro", "Platino", "Diamante"];
if (!validos.includes(nuevo)) {
// rechazamos lo que no toca
console.log("Rango inválido: " + nuevo);
// no guardamos basura
return;
}
// valor válido: lo guardamos
this._rango = nuevo;
},
};
// pasa la validación
heroe.rango = "Platino";
// 'Platino'
console.log(heroe.rango);
// no existe -> el setter lo rechaza
heroe.rango = "Leyenda";
// sigue 'Platino'
console.log(heroe.rango);Funcionan igual dentro de una clase (las del nivel 2): un get winrate() entre los métodos se usa
como instancia.winrate, sin paréntesis. Es la forma estándar de exponer valores calculados en una
API de objeto limpia.
Proxy y Reflect: interceptar los accesos#
Lo más avanzado del capítulo: el objetivo es reconocerlo y saber qué resuelve, no implementarlo desde cero. Un Proxy
envuelve un objeto y te deja interceptar lo que se hace con él —leer una propiedad, escribirla,
comprobar si existe— mediante funciones llamadas traps. El ejemplo clásico: registrar cada lectura.
const datos = { nombre: "Mercy", rol: "Apoyo" };
// new Proxy(objetoReal, traps). La trap 'get' se dispara en CADA lectura.
const observado = new Proxy(datos, {
get(obj, clave) {
// nuestra lógica: registrar
console.log("acceso a: " + String(clave));
// …y delegar la lectura real en Reflect
return Reflect.get(obj, clave);
},
});
// "acceso a: nombre", luego "Mercy"
console.log(observado.nombre);
// "acceso a: rol", luego "Apoyo"
console.log(observado.rol);Reflect es el compañero del Proxy: ofrece las operaciones internas sobre objetos como funciones
normales (Reflect.get, Reflect.set, Reflect.has). Dentro de una trap lo usas para delegar en el
comportamiento por defecto sin reimplementarlo: tú añades tu lógica (registrar, validar) y Reflect
hace el trabajo de siempre.
El ejemplo de arriba solo observa, pero una trap también puede decidir qué devolver. Un caso
real: un diccionario de traducciones (i18n) que, cuando le pides una clave que no existe, responde con
un aviso claro en vez de un undefined que acabaría pintándose en la pantalla:
// las traducciones que tenemos
const textos = { saludo: "Hola", adios: "Adiós" };
const t = new Proxy(textos, {
get(obj, clave) {
// existe: la traducción de siempre
if (clave in obj) return obj[clave];
// no existe: aviso, nunca un undefined silencioso
return "[falta: " + String(clave) + "]";
},
});
// 'Hola' -> la clave existe
console.log(t.saludo);
// '[falta: cerrar]' -> la trap devuelve el aviso, no undefined
console.log(t.cerrar);Cambiar lo que pasa al leer una clave que falta —en vez de tragarte un undefined— es metaprogramación
de la útil: el objeto que envuelves no cambia, pero su comportamiento desde fuera sí.
Esto, multiplicado, es el motor de la reactividad de Vue 3 y MobX: envuelven tus datos en un
Proxy, detectan qué lees durante un render y, cuando algo cambia, vuelven a pintar solo lo que
dependía de ese dato. La idea importante: las librerías hacen esto por ti; tú trabajas con objetos
normales. Y al revés: no metaprogramar por deporte. Un Proxy donde basta un get es complejidad
que alguien tendrá que mantener.
Pruébalo tú#
En la consola: un Symbol que no colisiona y se esconde del JSON, un get winrate que se actualiza
solo al cambiar los datos, un Proxy que registra cada lectura y otro que devuelve un valor por defecto
cuando falta una clave. Cambia las victorias de tracer y mira cómo el winrate se recalcula; lee otra
propiedad del espia y observa el registro; pide a t una clave que no exista y mira el aviso. Pulsa Ejecutar (o Ctrl+Enter) para ver la consola.
Comprueba lo que sabes#
Pregunta 1 de 5
¿Qué garantiza `Symbol()`?
Tu turno#
Modela la ficha del héroe: winrate que siempre cuadra, nota del scout que no se filtra al backend, y
una guardia en registrarVictoria con el WeakSet que el starter ya te da para evitar contar la
misma victoria dos veces. Empieza por la versión directa y sube de nivel con un getter y una clave
privada. Cuando lo tengas (o si te atascas), despliega las soluciones y fíjate en cómo el nivel
Excelente saca los datos privados a un WeakMap y razona cuándo esa maquinaria sobra.
Ejercicio · en esta página
Héroe con winrate calculado y metadatos privados
Convierte el roster en bruto en objetos de la app donde el winrate se calcule solo (siempre coherente con victorias y partidas) y la nota del scout quede privada: no debe aparecer en lo que se serializa para el backend (JSON.stringify), pero sí seguir accesible para el equipo. Además, usa el WeakSet `procesados` del starter para que `registrarVictoria` no pueda registrar la misma victoria dos veces en la misma sesión.
Paso 1: Que funcione
- Se muestra la plantilla por la consola con su winrate y la nota del scout.
- Vale calcular el winrate como una propiedad fija y la nota como propiedad normal.
- registrarVictoria usa el WeakSet para no contar una victoria dos veces.
- Comprendes el problema: la nota se filtra en el JSON y el winrate se desfasa.
Paso 2: Que esté pulido
- El winrate es un getter (get winrate()): siempre coherente, sin recalcular a mano.
- La nota vive bajo una clave Symbol y no aparece en JSON.stringify ni en Object.keys.
- La nota sigue siendo legible para quien tenga el símbolo.
- registrarVictoria usa el WeakSet para bloquear duplicados.
Paso 3: Que sea excelente
- Los datos privados viven en un WeakMap: la API pública del objeto queda limpia.
- registrarVictoria usa el WeakSet: la marca no toca el objeto y se libera sola si el héroe deja de usarse.
- Opcional: un Proxy que valide alguna escritura (p. ej. victorias <= partidas).
- Comentas cuándo esta maquinaria sobra: criterio, no metaprogramar por deporte.
Ver soluciones
// ════════════════════════════════════════════════════════════════════════════
// NIVEL OK — "funciona"
//
// winrate es una propiedad FIJA (se calcula una vez al crear el héroe) y la nota
// del scout es una propiedad normal. El resultado se ve, pero...
// - tras registrarVictoria(Tracer), su V/P pasa a 79/121 pero el winrate sigue
// marcando 65.0% (78/120): se quedó OBSOLETO. Habría que recalcularlo a mano
// en cada cambio, y olvidarse una sola vez es un bug silencioso.
// - la nota es una propiedad más, así que VIAJA en JSON.stringify: el payload
// del backend filtra un dato interno del equipo.
// Funciona, pero deja dos trampas que getters y claves privadas resuelven de raíz.
// ════════════════════════════════════════════════════════════════════════════
const ROSTER = [
{
nombre: "Tracer",
rol: "Daño",
victorias: 78,
partidas: 120,
nota: "Agresiva; brilla en mapas de control",
},
{
nombre: "Mercy",
rol: "Apoyo",
victorias: 130,
partidas: 200,
nota: "Pocket healer; vigilar el posicionamiento",
},
{
nombre: "Reinhardt",
rol: "Tanque",
victorias: 51,
partidas: 90,
nota: "Buen escudo; rota lento",
},
{
nombre: "Ana",
rol: "Apoyo",
victorias: 66,
partidas: 110,
nota: "Sleep dart decisivo; objetivo prioritario rival",
},
];
// Conjunto de héroes cuya victoria ya se registró. WeakSet: solo objetos,
// referencias débiles; no impide que el objeto se libere si deja de usarse.
const procesados = new WeakSet();
function registrarVictoria(heroe) {
// si ya está marcado, ignoramos el intento duplicado
if (procesados.has(heroe)) {
console.log(heroe.nombre + " ya procesado en esta sesión");
return;
}
// lo marcamos para no repetirlo
procesados.add(heroe);
// una victoria más
heroe.victorias++;
// y una partida más
heroe.partidas++;
}
function crearHeroe(raw) {
return {
nombre: raw.nombre,
rol: raw.rol,
victorias: raw.victorias,
partidas: raw.partidas,
// VALOR FIJO: se queda obsoleto si cambian las partidas
winrate: raw.victorias / raw.partidas,
// propiedad normal: aparece en JSON.stringify y en Object.keys
nota: raw.nota,
};
}
const heroes = ROSTER.map(crearHeroe);
// Tracer juega una más… pero su winrate ya no se entera
registrarVictoria(heroes[0]);
// la leemos como una propiedad cualquiera
const notaInterna = heroes[0].nota;
// muestra la plantilla en la consola
console.log("--- Plantilla ---");
for (const h of heroes) {
console.log(
h.nombre +
" | " +
h.rol +
" | " +
h.victorias +
"/" +
h.partidas +
" | " +
(h.winrate * 100).toFixed(1) +
"%",
);
}
// el JSON filtra la nota
console.log("--- JSON del primer héroe (lo que viaja al backend) ---");
console.log(JSON.stringify(heroes[0], null, 2));
console.log("--- Nota interna del scout (no debe viajar) ---");
console.log(notaInterna); Por qué este nivel
- winrate es una propiedad fija y la nota una propiedad normal: lo más directo. El WeakSet en registrarVictoria evita el doble registro.
- Dos trampas: tras sumar una partida, el winrate se queda obsoleto (habría que recalcularlo a mano), y la nota interna VIAJA en JSON.stringify.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL MEJOR — "pulido"
//
// Por qué mejora a OK:
// - winrate es un GET: se calcula desde victorias/partidas cada vez que se lee,
// así que SIEMPRE es coherente. Tras registrarVictoria(Tracer), su 79/121 se
// refleja al instante (65.3%), sin recalcular nada a mano.
// - la nota vive bajo una clave Symbol. JSON.stringify y Object.keys IGNORAN las
// claves de tipo Symbol, así que la nota deja de filtrarse al backend, pero
// sigue ahí para quien tenga el símbolo.
//
// Su límite respecto a Excelente: la nota sigue siendo una propiedad del objeto
// (oculta, pero presente); cualquiera con acceso al símbolo la lee o la pisa. Si
// quieres datos de verdad EXTERNOS al objeto, el WeakMap es más limpio.
// ════════════════════════════════════════════════════════════════════════════
const ROSTER = [
{
nombre: "Tracer",
rol: "Daño",
victorias: 78,
partidas: 120,
nota: "Agresiva; brilla en mapas de control",
},
{
nombre: "Mercy",
rol: "Apoyo",
victorias: 130,
partidas: 200,
nota: "Pocket healer; vigilar el posicionamiento",
},
{
nombre: "Reinhardt",
rol: "Tanque",
victorias: 51,
partidas: 90,
nota: "Buen escudo; rota lento",
},
{
nombre: "Ana",
rol: "Apoyo",
victorias: 66,
partidas: 110,
nota: "Sleep dart decisivo; objetivo prioritario rival",
},
];
// Una clave única para la nota. Como es un Symbol, no choca con ninguna otra
// propiedad y queda fuera de JSON.stringify / Object.keys.
const NOTA = Symbol("notaScout");
// Conjunto de héroes cuya victoria ya se registró. WeakSet: solo objetos,
// referencias débiles; no impide que el objeto se libere si deja de usarse.
const procesados = new WeakSet();
function registrarVictoria(heroe) {
// si ya está marcado, ignoramos el intento duplicado
if (procesados.has(heroe)) {
console.log(heroe.nombre + " ya procesado en esta sesión");
return;
}
// lo marcamos para no repetirlo
procesados.add(heroe);
// una victoria más
heroe.victorias++;
// y una partida más
heroe.partidas++;
}
function crearHeroe(raw) {
const heroe = {
nombre: raw.nombre,
rol: raw.rol,
victorias: raw.victorias,
partidas: raw.partidas,
get winrate() {
// se calcula al LEERLO: nunca un valor obsoleto
return this.victorias / this.partidas;
},
};
// la nota bajo la clave Symbol: invisible para JSON/keys
heroe[NOTA] = raw.nota;
return heroe;
}
const heroes = ROSTER.map(crearHeroe);
// Tracer juega una más y su winrate se actualiza solo
registrarVictoria(heroes[0]);
// la leemos con el símbolo (quien lo tenga, accede)
const notaInterna = heroes[0][NOTA];
// muestra la plantilla en la consola
console.log("--- Plantilla ---");
for (const h of heroes) {
console.log(
h.nombre +
" | " +
h.rol +
" | " +
h.victorias +
"/" +
h.partidas +
" | " +
(h.winrate * 100).toFixed(1) +
"%",
);
}
// el JSON ya NO trae la nota
console.log("--- JSON del primer héroe (lo que viaja al backend) ---");
console.log(JSON.stringify(heroes[0], null, 2));
console.log("--- Nota interna del scout (no debe viajar) ---");
console.log(notaInterna); Por qué es mejor que el anterior
- winrate es un getter: se recalcula al leerlo, así que siempre cuadra con victorias/partidas, sin recalcular nada a mano.
- La nota vive bajo una clave Symbol, que JSON.stringify y Object.keys ignoran: deja de filtrarse al backend pero sigue accesible con el símbolo.
- El WeakSet marca los héroes procesados sin tocar el objeto: la guardia en registrarVictoria no deja rastro en la API pública.
- Su límite: la nota sigue siendo una propiedad del objeto (oculta, pero ahí). Para datos de verdad externos, el WeakMap es más limpio.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL EXCELENTE — "óptimo"
//
// Por qué mejora a Mejor:
// - La nota deja de ser una propiedad del objeto: vive en un WeakMap (notas),
// asociada al héroe DESDE FUERA. El objeto público solo tiene nombre, rol,
// victorias, partidas y el get winrate: una API limpia, sin datos colados.
// Y como el WeakMap usa referencias débiles, si un héroe deja de usarse, su
// nota se libera sola: cero fugas de memoria.
// - (Opcional) Un Proxy valida las escrituras: bloquea dejar victorias por
// encima de partidas. La regla de negocio vive en un sitio, no esparcida.
//
// Cuándo esto SOBRA: para una ficha sencilla, get + Symbol del nivel Mejor ya
// resuelven. El WeakMap brilla cuando los datos privados son muchos o no deben
// tocar el objeto; el Proxy, solo si de verdad necesitas interceptar accesos. No
// metaprogrames por deporte: cada herramienta donde su ventaja se note.
// ════════════════════════════════════════════════════════════════════════════
const ROSTER = [
{
nombre: "Tracer",
rol: "Daño",
victorias: 78,
partidas: 120,
nota: "Agresiva; brilla en mapas de control",
},
{
nombre: "Mercy",
rol: "Apoyo",
victorias: 130,
partidas: 200,
nota: "Pocket healer; vigilar el posicionamiento",
},
{
nombre: "Reinhardt",
rol: "Tanque",
victorias: 51,
partidas: 90,
nota: "Buen escudo; rota lento",
},
{
nombre: "Ana",
rol: "Apoyo",
victorias: 66,
partidas: 110,
nota: "Sleep dart decisivo; objetivo prioritario rival",
},
];
// Almacén privado: asocia cada héroe (la clave es el OBJETO) con su nota, fuera
// del objeto. Débil: cuando el héroe ya no se use, su entrada se libera sola.
const notas = new WeakMap();
// Conjunto de héroes cuya victoria ya se registró. WeakSet: solo objetos,
// referencias débiles; no impide que el objeto se libere si deja de usarse.
const procesados = new WeakSet();
function registrarVictoria(heroe) {
// si ya está marcado, ignoramos el intento duplicado
if (procesados.has(heroe)) {
console.log(heroe.nombre + " ya procesado en esta sesión");
return;
}
// lo marcamos para no repetirlo
procesados.add(heroe);
// una victoria más
heroe.victorias++;
// y una partida más
heroe.partidas++;
}
function crearHeroe(raw) {
const heroe = {
nombre: raw.nombre,
rol: raw.rol,
victorias: raw.victorias,
partidas: raw.partidas,
get winrate() {
// calculado, siempre coherente
return this.victorias / this.partidas;
},
};
// (Opcional) Proxy de validación: intercepta las escrituras. Si alguien intenta
// dejar victorias por encima de partidas, la rechaza en vez de corromper el dato.
const heroePublico = new Proxy(heroe, {
set(obj, clave, valor) {
if (clave === "victorias" && valor > obj.partidas) {
console.log(
"Bloqueado: " + valor + " victorias > " + obj.partidas + " partidas",
);
// ignoramos la escritura inválida, sin lanzar error
return true;
}
// lo válido: delega en el comportamiento por defecto
return Reflect.set(obj, clave, valor);
},
});
// La nota se guarda FUERA del objeto, en el WeakMap, asociada al héroe PÚBLICO
// (el Proxy es lo que circula por la app: la clave del WeakMap debe ser ese mismo
// objeto, no el interno, o luego notas.get(heroe) no encontraría nada).
notas.set(heroePublico, raw.nota);
return heroePublico;
}
const heroes = ROSTER.map(crearHeroe);
// Tracer juega una más; el get winrate se mantiene coherente
registrarVictoria(heroes[0]);
// leemos la nota del WeakMap, no del objeto
const notaInterna = notas.get(heroes[0]);
// muestra la plantilla en la consola
console.log("--- Plantilla ---");
for (const h of heroes) {
console.log(
h.nombre +
" | " +
h.rol +
" | " +
h.victorias +
"/" +
h.partidas +
" | " +
(h.winrate * 100).toFixed(1) +
"%",
);
}
// el JSON sale limpio
console.log("--- JSON del primer héroe (lo que viaja al backend) ---");
console.log(JSON.stringify(heroes[0], null, 2));
console.log("--- Nota interna del scout (no debe viajar) ---");
console.log(notaInterna);
// Demostración del Proxy: este intento inválido se bloquea (mira la consola).
// 9999 > partidas -> rechazado, el dato no se corrompe
heroes[0].victorias = 9999; Por qué es mejor que el anterior
- La nota vive en un WeakMap, fuera del objeto: la API pública queda limpia y, por las referencias débiles, no hay fuga de memoria.
- El WeakSet de procesados aplica la misma filosofía: la marca no toca el objeto y se libera sola si el héroe deja de usarse.
- Un Proxy de validación bloquea dejar victorias por encima de partidas: la regla de negocio en un solo sitio.
- Y criterio: comenta cuándo esto sobra. Para un caso simple, get + Symbol bastan; no se metaprograma por deporte.