Template Method Pattern. Partiendo otra lanza por los clásicos.

Ya hacía unos meses que no pasaba por aquí, pero empieza otra etapa en la vida mucho más despejada. Quería escribir algunos nuevos propósitos, pero hablando con un colega me acordé que tenía este borrador por algún lado pendiente.

Seguimos luchando contra dogmatismos y acercándonos al día que escriba sobre Composition over Inheritance (lo siento, hoy tampoco es ese día…)

Entre los perfiles que leen este blog, cuando hablo de programación orientada a objetos (OOP), hay dos posiciones muy comunes con preguntas prácticamente opuestas:

  • ¿Por qué hablas como si la OOP clásica hubiera muerto?
  • ¿Por qué hablas como si alguien usase mecanismos clásicos de OOP en 2026?
NOTA: me tomo una licencia con mi idioma, de usar formas del adjetivo "clásico" no solo con su acepción original, sino también con una sobrecarga de significado: "relacionado con las clases". A Don Pérez Reverte le estará picando la nariz. Me la suda.

La primera pregunta es común en los perfiles que siguen trabajando en ecosistemas donde los mecanismos clásicos de la OOP son el pan de cada día, especialmente con lenguajes como Java o C#.

La segunda se la hacen los perfiles que llevan más años en el frente del estado del arte: la ingeniería de software moderna potenciada por mecanismos superflexibles y lenguajes de cadena de prototipo (te estoy mirando a ti, ECMAScript), la que es completamente capaz de solventar todos los problemas que daban razón de ser a los mecanismos clásicos de OOP, como las clases y la herencia.

El suceso definitorio: la postura de Evan You

El mismo Evan You, creador de Vue.js, reorientó toda la arquitectura de Vue 3 y Vite (especialmente en la Composition API), a evadir completamente las clases. Tenía muchas razones para ello bien explicadas, empezando porque las clases no existen como tal en JavaScript, solo son sugars sintáctico que llena de hacks el código y problemas de rendimiento. O al menos así era en aquella época.

Esa sensación no era nueva en el mundillo. Los que sabíamos la verdad™, sobre cómo construir funciones y definir su cadena de prototipo, sabíamos que algo olía raro en esa sintaxis. Las palabras de Evan You reforzaron a una nueva generación que se alejó de las clases y empezó a experimentar con mecanismos y patrones que no encajaban en los modelos clásicos.

Es un error garrafal pensar que la programación orientada a objetos va de “clases”. Las clases son un mecanismo más. Los verdaderos protagonistas son los objetos, y hay más de una forma de definirlos, construirlos y relacionarlos.

Pero es un error aún más grave pensar que las clases son malas o que no se deben usar, o que sus mecanismos asociados como la herencia son peligrosos para la ingeniería de software.

Por supuesto esto no es lo que defiende Evan You, él solo explicaba los motivos en los contextos de JavaScript y en sus propias arquitecturas, no inició ninguna especie de cruzada en contra como algunos parece que asumieron.

El (no tan) problema de las clases, abstracción y herencia

Soy muy fan de la composición, y también prometo que no tengo ningún problema personal ni ningún juicio negativo contra mecanismos como las clases y la herencia. Lo demostraré.

¿Has oído hablar de eso del principio abierto/cerrado (OCP)? Seguramente, como todo el gremio, la O de los SOLID, la joyita de Bertrand Meyer favorita del tito Bob Martin, y el principio que menos entendemos y menos sabemos explicar.

Ese principio es, en realidad, en el que se fundamentan aquellos patrones que buscan erradicar los problemas más graves de la abstracción en clases, y hoy voy a rescatarlo con otro ejemplo más real.

Caso real: creando un sistema de componentes UI

Imagínate, en el viejo mundo, que definimos una clase abstracta BaseComponent, donde queremos definir su ciclo de vida (creación, montaje, renderizado, desmontaje).

 abstract class BaseComponent{
    root: HTMLElement

    constructor(options: BaseComponentProps) {
    	this.root = document.createElement(options.root) 
    }
    
    mount(target: HTMLElement) {
    	target.appendChild(this.root)
    }

    unmount() {
    	this.root.remove()
    }

    abstract render(): void
}

Decidimos que render sea abstracto, parte del contrato, cada componente se renderiza como quiere así que debe implementarlo. Pregunta, ¿esto respeta el principio abierto/cerrado?

Aquí nacen los verdaderos problemas de la abstracción y la herencia y por qué todo empieza a irse de madres rápidamente.

El principal problema no está en los abstractos, a mí me es irrelevante si quieres dejar funciones dummies o si te cargas la palabra abstract de todos lados, solo lo puse para despistar.

Supón que el primer componente que creas, tiene que hacer cosas durante el montaje o el desmontaje. ¿Qué haces?

“Fácil, sobreescribo el método mount y listo, primero llamando al método padre para respetar lo que hiciera antes…“.


// En una clase que hereda de BaseComponent
// ...
mount(target: HTMLElement) {
    super.mount(target)
    // Hago cosas específicas de este componente durante el montaje
}
// ...

Esto tiene una buena cantidad de problemas:

  • Tienes que llamar al supermétodo. Aquí de por sí encontramos otro conjunto de problemas, derivados del simple hecho de darte esa responsabilidad (empezando por lo obvio: que no te olvides de llamarla)
  • El punto más crítico es que estás controlando el flujo de vida del componente, modificando su comportamiento. Ok, no lo estamos haciendo, pero podríamos, y eventualmente tendremos necesidad o tentación de hacerlo. Aquí es cuando entran los líos, se introducen sorpresas y se empieza a dispersar responsabilidades por todos lados, este es el escenario al que nunca queremos llegar. Es al que todo el mundo teme.
    • Un caso habitual ocurre cuando decide posponerse la llamada al padre, primero hacer algo, y al final invocar al supermétodo. A lo mejor funciona, pero puede que tras un cambio de la implementación del padre, la nueva acción no se pueda realizar antes, quizás necesita de algo que solo se construye tras un primer mount. Nadie definió que pudieras hacer eso, y pagas cualquier side effect.

¿Qué tal si decidimos que el ciclo de vida del componente sea inmutable? Que sí puedas definir qué hacer tras cada paso, sin saber siquiera lo que hacen de base ni tener la opción de modificarlo (ni de olvidarte). ¿Cómo podemos entonces extender el comportamiento sin modificar y sin tener la posibilidad de meter la pata?

El Template Method Pattern

Es un patrón con muchas variantes, pero todas ellas se basan en la misma idea: mantener un flujo y dejar pasos concretos a otros sin posiblidad de alterar el flujo.

Para este caso se definen métodos adicionales que representan qué hacer tras el paso concreto en el ciclo de vida. Por ejemplo, métodos anclaje como onMount() u onRender(), que se llamarán tras el montaje o el renderizado respectivamente, pero que NO HACEN NADA por sí mismos.


// En BaseComponent
readonly mount = (target: HTMLElement) => {
    target.appendChild(this.root)
    this.onMount()
}

onMount() {}

De ahí la palabra “Template”. Básicamente ofreces una plantilla de ejecución, donde tipos concretos de la clase solo meten el código propio en los puntos claves del ciclo de vida.

También podemos controlar el orden, meter otra forma de anclar al ciclo de vida antes o después de cada punto, como beforeMount() o afterMount().

Lo importante es que el componente hijo no sabría de detalles de ejecución ni modifica el comportamiento del padre, pero sí puede extenderlo. Es el ejemplo realista más explícito de OCP que he encontrado recientemente.

¿Notaste el “sutil” cambio de sintaxis?

Volvamos con esas quejas de Evan You.

Nótese que en TypeScript hemos cambiado el método mount a una función de flecha de solo lectura, para evitar su sobreescritura. En lenguajes más tradicionales enfocados a OOP tenemos palabros como final o sealed para marcar métodos como no sobrescribibles. En JavaScript, no. Como decíamos, en JavaScript en realidad no existen ni las clases como tal.

En TypeScript no tenemos esto para métodos, pero sí para campos. Por alguna razón se les resiste en métodos, de ahí lo de pasarlo a un campo readonly y función flecha (y esto no es gratis, ojo, no es un mero cambio de sintaxis).

Dicen que es un debate complicado por la incongruencia entre ejecución y compilación, pero lo cierto es que ya existen mecanismos restrictivos en TypeScript como ReadonlySet, para definir un Set como de solo lectura, y que en ejecución no deja de ser un Set normal y corriente.

Si te gusta la DX, seguramente ya estés pensando que se podría añadir una regla a ESLint o un plugin de TypeScript para que se pueda usar readonly, final, sealed o algún otro palabro, o un decorador. Y así es, yo meto metaprogramación por DX en mis proyectos como si no hubiera mañana.

Middleware, eventos, composición funcional, clausuras, inyección de dependencias…

A la que tengas un poco de conocimiento ya se te habrán ocurrido muchas otras formas de solucionar el mismo problema, muchas que entran en el camino de la Composión sobre la Herencia (COI), y algunas que ni requieren clases (en lenguajes que permitan prescindir de ellas).

A mí personalmente me encanta trabajar con eventos y listeners (o cualquier variante de observer o pub/sub), los prefiero a todo esto. Pero he hecho ya una buena cantidad de auditorías, las suficientes como para garantizarte que pueden usarse mecanismos, patrones y arquitecturas clásicas exactamente igual de eficientes y fáciles de mantener.

El código más fácil de explicar es evidentemente el que no requiere explicación, y la herencia es algo que se suele entender fácilmente (quizás tiene algo que ver el que la metan hasta en la sopa sin ningún contexto en FP y primeros años de universidad). Para mí es una herramienta de diseño de muy alto valor, y la despreciamos porque no nos paramos a pulirla un poquito o porque nos suena a algo viejo o “demasiado fácil”.

La mayoría de opiniones que veo en estos debates están poco justificadas y cargadas de dogma. Para mí es una guerra donde no hay botín, y a la que cuaquier persona con un mínimo de conocimiento le importará un pimiento mientras exista una buena especificación y batería de tests. Aconsejo un poco más de la infalible tolerancia y pragmatismo.