Módulo 2: Cuándo NO abstraer

4. Abstracción prematura: el error más caro

Descripción

Al terminar esta lección vas a poder defender, con números y sin sentirte hereje, una afirmación que suena mal la primera vez que se escucha: duplicar dos veces suele ser más barato que abstraer mal una vez. Vas a ver por qué la duplicación es un problema visible, local y reversible, mientras que la abstracción equivocada es invisible, sistémica y se hereda. Vas a conocer el ciclo de degradación que convierte una abstracción razonable en un monstruo con siete parámetros, y vas a salir con el procedimiento para deshacerla, que empieza con un movimiento contraintuitivo: volver a duplicar.

Esto importa porque en la formación de casi cualquier desarrollador hay un principio grabado a fuego —no te repitas— y no hay ninguno que lo contrapese. El resultado es un sesgo sistemático: frente a dos códigos parecidos, la respuesta por defecto es unificarlos, y esa respuesta se toma sin evaluar el costo. Esta lección instala el contrapeso, y no para que abandones el principio —que es bueno— sino para que lo apliques a lo que de verdad se refiere.

La lección 3 terminó con una cuenta que quedó abierta. En Boletia, no haber abstraído los proveedores de pago habría dejado tres funciones duplicadas; haber abstraído mal dejó catorce sitios acoplados a una convención escondida. Tres contra catorce. Esta lección explica por qué esa asimetría no es una casualidad de ese caso, sino la forma normal del problema.

Conexión con el módulo: la lección 2 te dio el costo de una abstracción cualquiera; la 3 te dijo cuándo tienes información para pagarlo. Esta lección se ocupa del caso en el que no esperaste y elegiste el eje equivocado: qué forma toma el daño, por qué es tan difícil de revertir, y cómo se revierte. La lección 5 va a atacar la motivación que produce este error —construir para un futuro imaginado— y la lección 6 va a mostrar su versión extrema: abstraer con cero casos de variación. El procedimiento de salida que verás aquí es, literalmente, el que vas a ejecutar en el proyecto de la lección 8.

La mancha y la viga

Tienes una camisa con una mancha de café en el frente. Es fea, se ve desde lejos, todo el mundo coincide en que está ahí, y quitarla es un procedimiento conocido: agua, jabón, quince minutos. Si el remedio no funciona, la camisa queda igual que antes y puedes probar otro. El problema es visible, local y reversible.

Ahora tienes una casa donde alguien, hace años, puso una viga en medio de la sala. No sabes si carga peso. Está detrás de un muro, así que no se ve. El plano original se perdió. Tú quieres tirar el muro para abrir el espacio, y en el momento en que tocas la viga, la pregunta ya no es de decoración: es "¿se me cae el techo?". Nadie en la casa lo sabe. Y como nadie lo sabe, la respuesta práctica de todos los habitantes durante años ha sido la misma: no tocarla, y acomodar los muebles alrededor.

La duplicación es la mancha. La abstracción equivocada es la viga.

Y hay una diferencia más, que es la que decide el resultado a largo plazo: la mancha te molesta a ti; la viga la heredan los que vienen. Quien llega nuevo a la casa ve la viga, asume que está ahí por una razón estructural, y acomoda sus muebles igual que todos. Nunca se le ocurre que podría no cargar nada. La viga se vuelve parte del edificio no porque sea necesaria, sino porque nadie sabe si lo es.

Ese mecanismo tiene nombre y es más viejo que el software.

La cerca de Chesterton

G. K. Chesterton, hace un siglo, planteó una parábola sobre reformas. Imagina que encuentras una cerca cruzando un campo, aparentemente sin propósito. La reacción del reformador impaciente es "esto no sirve para nada, quitémosla". La respuesta de Chesterton fue: no la quites hasta que sepas por qué la pusieron. Quizá había un toro del otro lado.

Es un principio excelente, y lo defiendo: en general, no toques lo que no entiendes. Es la base de la lección "leer antes de tocar" del módulo 8.

Pero fíjate en su efecto secundario, porque es exactamente el mecanismo por el cual las abstracciones sobrantes se vuelven permanentes. La cerca de Chesterton supone que la cerca tuvo una razón. En el software, muchas veces la razón fue "leí un artículo el fin de semana" o "un cliente dijo que quizá". La cerca existe, nadie recuerda por qué, y el principio prudente —no la quites sin saber— se convierte en una garantía de permanencia, porque averiguar por qué está cuesta más que acomodar los muebles.

Así que necesitamos la versión completa del principio, que es la que vas a usar en el proyecto:

No quites la cerca hasta saber por qué está. Pero averígualo. Y si la respuesta es "no hay razón, o la razón dejó de existir", quítala.

La diferencia entre prudencia y parálisis está en el verbo "averígualo". Un equipo sano investiga y decide; un equipo paralizado acomoda muebles durante años.

Ejemplo trabajado: los mensajes de confirmación de Boletia

Vamos a ver el fenómeno completo en un rincón pequeño, donde se puede seguir línea por línea.

El requisito. Cuando alguien compra en Boletia, hay que avisarle. Al principio por dos canales: correo y SMS. Los dos mensajes dicen sustancialmente lo mismo.

Lo que se hizo, con dos canales sobre la mesa.

# Archivo: notifications/message.py   ← escrito cuando había DOS canales
from abc import ABC, abstractmethod

class MessageBuilder(ABC):
    """Arma el texto del mensaje de confirmación de una compra."""

    @abstractmethod
    def build(self, order) -> str:
        """Devuelve el texto listo para enviar."""

Con correo y SMS, esta abstracción es impecable. Los dos producen texto, los dos reciben una orden, los dos dicen lo mismo con más o menos palabras. Cualquier revisor la habría aprobado, y con razón.

Llega el tercer canal: push.

La notificación push no es texto. Es una estructura con tres partes: un título de máximo cuarenta caracteres, un cuerpo de máximo ciento veinte, y un bloque de datos con el enlace profundo que abre la app en la pantalla correcta. Además, no admite enlaces en el cuerpo, y el correo sí los quiere.

En vez de rediseñar —que implicaría tocar el código que ya usa build()— se hizo lo de siempre. Vamos a ver el estado de la interfaz hoy, después de cuatro rondas de ajustes:

# Archivo: notifications/message.py   ← el mismo archivo, dos años después
from abc import ABC, abstractmethod

class MessageBuilder(ABC):
    """Arma el mensaje de confirmación de una compra."""

    @abstractmethod
    def build(
        self,
        order,
        max_length: int | None = None,   # lo usa SMS (160) y push (120). Email lo ignora.
        include_links: bool = True,      # lo usa email. SMS lo pone en False. Push lo ignora.
        plain_text: bool = False,        # lo usa email. Los otros dos lo ignoran.
    ) -> str:
        """Devuelve el texto del mensaje."""

    def title(self, order) -> str | None:
        return None                      # solo push devuelve algo. Los otros dos, None.

    def payload_extras(self, order) -> dict:
        return {}                        # solo push devuelve algo. Los otros dos, vacío.

    def supports_html(self) -> bool:
        return False                     # solo email devuelve True.

Cuenta lo que hay ahí: tres parámetros y tres métodos extra, y cada uno de los seis le sirve a exactamente una de las tres implementaciones. Ninguna implementación usa más de tres de los seis. Es decir: la interfaz "común" está compuesta casi enteramente de cosas que no son comunes.

Ahora mira el sitio de llamada, que es donde la factura se cobra:

# Archivo: notifications/notifier.py  (fragmento, hoy)
builder = get_builder(channel)

if channel == "push":
    # Push necesita título y datos aparte; los otros dos no.
    body = builder.build(order, max_length=120)
    payload = {
        "title": builder.title(order),
        "body": body,
        **builder.payload_extras(order),
    }
    push_channel.send(customer.push_token, payload)

elif channel == "email":
    body = builder.build(order, plain_text=not builder.supports_html())
    email_channel.send(customer.email, body)

else:  # sms
    body = builder.build(order, max_length=160, include_links=False)
    sms_channel.send(customer.phone, body)

Detente aquí, porque este fragmento es la lección entera en veinte líneas.

El condicional por canal está de vuelta. Toda la razón de ser de MessageBuilder era que quien envía no tuviera que saber qué canal es. Y ahí está el if channel ==, vivito. Pero es peor que el condicional original que la abstracción vino a eliminar, porque ahora el llamador tiene que saber dos cosas: qué canal es, y con qué parámetros hay que alimentar a la abstracción para ese canal. Antes solo tenía que saber una.

Ese es el patrón que quiero que reconozcas: cuando una abstracción está en el eje equivocado, el condicional no desaparece, se muda al llamador y se vuelve más complicado.

Ahora la versión honesta. Lo mismo, sin abstracción, tres funciones en los archivos que ya existían:

# Archivo: notifications/email_channel.py
def build_confirmation_email(order):
    """Cuerpo del correo de confirmación. Admite HTML y no tiene límite de largo."""
    total = format_money(order.total)
    link = f"{settings.BASE_URL}/orders/{order.id}"
    return (
        f"<p>Hola {order.customer_name},</p>"
        f"<p>Tu compra por <b>{total}</b> quedó confirmada.</p>"
        f'<p><a href="{link}">Ver mis boletos</a></p>'
    )


# Archivo: notifications/sms_channel.py
def build_confirmation_sms(order):
    """Texto del SMS. Tope de 160 caracteres y sin enlaces: los operadores los recortan."""
    total = format_money(order.total)
    return f"Boletia: tu compra por {total} quedo confirmada. Orden #{order.id}"


# Archivo: notifications/push_channel.py
def build_confirmation_push(order):
    """Carga de la notificación push: título corto, cuerpo corto y enlace profundo."""
    total = format_money(order.total)
    return {
        "title": "Compra confirmada",                        # el sistema corta a 40 caracteres
        "body": f"Tu compra por {total} está lista."[:120],
        "data": {"deep_link": f"boletia://orders/{order.id}"},
    }

Y el sitio de llamada:

# Archivo: notifications/notifier.py  (versión honesta)
if channel == "push":
    push_channel.send(customer.push_token, build_confirmation_push(order))
elif channel == "email":
    email_channel.send(customer.email, build_confirmation_email(order))
else:
    sms_channel.send(customer.phone, build_confirmation_sms(order))

Sigue habiendo un condicional por canal —eso es honesto, hay tres canales y son distintos— pero ahora cada rama es una línea legible y no hay que saber con qué parámetros alimentar nada.

Pongamos los dos caminos lado a lado:

Abstraída malDuplicada
Archivos nuevos4 (message.py + 3 builders)0 (funciones en archivos existentes)
Conceptos nuevos del proyecto1 (MessageBuilder) + 6 miembros3 funciones con nombre autoexplicativo
Parámetros que cada implementación ignoraentre 2 y 3 de 3
Métodos que devuelven vacío2 de 3 implementaciones, en 2 métodos
Condicional por canal en el llamadorsí, con parámetrossí, de una línea por rama
Duplicación real que quedauna llamada a format_money y una frase parecida

Qué esperar de esta comparación. Tres cosas, y la tercera es la que decide.

Primero: la duplicación que queda es minúscula. Mira la última fila. Después de todo el drama, lo único que de verdad se repite en la versión honesta es format_money(order.total) —una llamada a una función que ya existe— y una frase en tres variantes que de todos modos tenían que ser distintas, porque una lleva HTML, otra cabe en 160 caracteres y la tercera en 120. Es decir: gran parte de lo que la abstracción intentaba unificar no era duplicación; era parecido superficial entre cosas que tienen restricciones distintas.

Segundo: la versión honesta se lee sin saltos. Para responder "¿qué dice el SMS de confirmación?", abres sms_channel.py y lees tres líneas. En la versión abstraída abres el builder de SMS, ves que llama a super().build(), vas a message.py, encuentras que el texto base está ahí, vuelves para ver qué sobrescribe el de SMS, y compruebas en notifier.py con qué parámetros se lo llamó. Cuatro pasos para leer una frase.

Tercero, y aquí está el remate. Llega una petición de marketing: "que el correo diga 'Tu compra está lista' en vez de 'quedó confirmada'. Solo el correo; el SMS y el push déjalos igual."

  • En la versión honesta: abres email_channel.py, cambias la frase, listo. Un archivo, una línea. Y —esto es lo importante— sabes con certeza que no tocaste los otros dos, porque no hay nada compartido que tocar.
  • En la versión abstraída: abres el builder de correo y descubres que la frase no está ahí. Está en message.py, en un método de la clase base, porque "era común a los tres". Cambiarla ahí cambia los tres. Cambiarla solo para el correo exige sobrescribir el método en el builder de email, lo cual duplica la frase base y deja el método padre con un caso menos que atender.

Lee esa última línea otra vez: tienes que tomar una decisión de diseño para cambiar una frase de marketing. Ese es el costo real de la abstracción equivocada, y no aparece en ninguna métrica. La duplicación te cuesta tiempo cuando hay que cambiar todo a la vez. La abstracción equivocada te cuesta tiempo cuando hay que cambiar una sola cosa — que es lo que pasa casi siempre.

Por qué la duplicación se ve y se arregla

Vamos a enumerar las propiedades de la duplicación, porque hay que ser justos con ella: es un problema, y tiene una forma muy amable.

Es visible. Puedes buscarla. grep, la búsqueda del editor, o directamente un detector de código repetido la encuentran. Cualquiera que abra el proyecto ve que dos bloques se parecen. No requiere arqueología.

Es local. Cada copia es independiente. Cambiar una no rompe otra. Puedes trabajar en una sin entender las demás, y sin miedo.

Su costo es proporcional y previsible. Tres copias, tres cambios. Duele de forma lineal, y —esto es lo bueno— el dolor avisa. Cada vez que tienes que cambiar lo mismo en tres lugares, el sistema te está mandando una señal clara y a tiempo. Los problemas que duelen linealmente son los problemas que se arreglan, porque el dolor llega antes que la catástrofe.

Su peor caso está acotado. ¿Qué es lo peor que puede pasar? Que una copia se actualice y otra no. Es un bug real, molesto y a veces caro. Pero es local, detectable y arreglable en un lugar. No se propaga.

Y la propiedad decisiva: la duplicación conserva la información. Cada copia sabe exactamente qué necesita su caso, sin negociar con nadie. Cuando llega el momento de unificar, tienes las tres versiones completas y honestas delante, y puedes ver qué comparten de verdad. Una abstracción prematura destruye esa información: en el momento en que metes tres casos en un molde, dejas de saber en qué eran distintos, porque las diferencias quedaron codificadas como parámetros y banderas.

Ese último punto es el que más se subestima. La duplicación no es solo tolerable: durante la fase en que todavía no sabes cuál es el eje, es la forma de almacenar los datos que necesitas para decidir bien.

Por qué la abstracción equivocada se hereda

Ahora el otro lado, con sus mecanismos uno por uno. Fíjate en que ninguno tiene que ver con la calidad del código: son mecanismos sociales y cognitivos, y por eso son tan difíciles de contrarrestar.

No se ve, y las métricas la premian. No existe un grep para "abstracción cuyo eje está mal". Ninguna herramienta te la señala. Y hay algo peor: los indicadores de calidad más usados la recompensan. Poca duplicación: bien. Clases pequeñas: bien. Alta cobertura de tests: bien, y de hecho suele estar bien testeada, porque las interfaces son fáciles de testear. Un tablero de calidad puede dar verde en todo mientras el código es imposible de cambiar.

Se hereda con el supuesto de que hay una razón. Es la cerca de Chesterton. Quien llega ve la estructura, supone que alguien más inteligente que él la puso por algo, y se adapta. Ese supuesto es razonable individualmente y desastroso colectivamente: si todos suponen que alguien más sabe, nadie sabe.

El camino de menor resistencia es agregar, no rediseñar. Cuando llega un caso que no encaja, tienes dos opciones. Agregar un parámetro: diez minutos, cambio pequeño, revisión fácil, cero riesgo aparente. Rediseñar: tres días, cambio grande, revisión difícil, riesgo alto. La opción racional a corto plazo es siempre la primera, y como cada decisión se toma por separado, nunca hay un momento en el que la opción cara sea la racional.

Cada uso nuevo la fija más. Cuanto más código se apoya en la abstracción, más caro es cambiarla; y cuanto más caro es cambiarla, más se prefiere escribir código nuevo que se apoye en ella en vez de arreglarla. Es un ciclo que se refuerza a sí mismo, y explica por qué el costo de quitar crece con el tiempo en vez de quedarse quieto.

La autoría se diluye. Quien la puso se fue o no se acuerda. Quien la sufre no se siente con derecho a tirar el trabajo de otro. Y la única persona que podría autorizar el rediseño necesita una justificación de negocio para tres días de trabajo que no agrega ninguna funcionalidad. Nadie es dueño de la viga.

El costo se atribuye mal. Este es el mecanismo más silencioso. Cuando el código es difícil de cambiar, la gente casi nunca culpa a la abstracción. Culpa al dominio: "es que el negocio de boletos es muy complicado". Culpa al lenguaje. Culpa a la falta de tiempo. Y como el diagnóstico está mal, el remedio también: se pide más documentación, más tests o más gente, y ninguna de las tres cosas toca el problema.

El ciclo de degradación

Junta los mecanismos anteriores y sale un ciclo que se repite igual en todos lados. Sandi Metz lo describió mejor que nadie; esta es su forma, paso por paso.

  1. Alguien abstrae con información parcial. Dos casos, una interfaz razonable. En ese momento es una buena decisión, o al menos una decisión defendible.
  2. Llega un requisito que casi encaja. Casi. Le falta algo o le sobra algo.
  3. Se agrega un parámetro, una bandera o un caso especial en vez de rediseñar. Diez minutos, revisión aprobada, todo el mundo contento.
  4. Se repiten los pasos 2 y 3 unas cuantas veces. Cada vuelta es individualmente razonable.
  5. La abstracción acumula parámetros y condicionales internos. Ahora hace muchas cosas parecidas y ninguna clara. Los nombres dejaron de describirla.
  6. Quien llega ve un desastre incomprensible, asume que esa complejidad es esencial al problema —porque, después de todo, alguien la escribió así— y cuando le toca su requisito, agrega su propio parámetro. Vuelta al paso 4.

Fíjate en la trampa del paso 6: la complejidad accidental se disfraza de complejidad esencial. Cada capa de deformación hace más creíble que el problema sea intrínsecamente difícil, y menos probable que alguien lo cuestione.

La señal de cada vuelta del ciclo

Hay una señal barata y muy confiable, y conviene volverla un reflejo:

Cada bandera booleana nueva en una firma es un condicional que le pasaste al llamador.

Cuando ves build(order, plain_text=False), lo que estás viendo es que adentro hay un if plain_text:. Y que quien llama tiene que decidir ese booleano, lo cual significa que quien llama tiene que saber algo sobre el interior de la función. La abstracción prometía que el llamador no tendría que saber. La bandera es la confesión de que la promesa se rompió.

Las señales, en orden de gravedad:

SeñalQué significa
Un parámetro opcional nuevoUn caso no encajaba y se hizo espacio a la fuerza
Una bandera booleanaHay un condicional adentro y el llamador tiene que conocerlo
Un método que devuelve None o {} en la mayoría de las implementacionesEl método pertenece a un caso, no a la familia
Una implementación que deshace algo que la interfaz le impusoEl eje está mal elegido
Un valor de retorno con formato especial que el llamador tiene que interpretarLa interfaz no puede expresar el caso; se está contrabandeando información
Un isinstance o una comprobación de tipo sobre la abstracciónLa abstracción no abstrae nada

Si encuentras tres o más de estas señales en la misma interfaz, no tienes una abstracción que necesita un ajuste. Tienes una abstracción que está en el eje equivocado, y ningún ajuste la va a arreglar.

La cuenta honesta

Vamos a poner los dos caminos frente a frente con el mismo escenario: tres casos parecidos que aparecieron en momentos distintos.

Camino A — duplicar dos veces.

  • Costo de escribir: escribes lo mismo tres veces. Poco, y con copiar y pegar, menos.
  • Costo de cargar: tres funciones independientes, cada una legible sola. Cero conceptos nuevos.
  • Riesgo: que una copia se desincronice de las otras. Real, local, detectable.
  • Costo de unificar más tarde: bajo. Las tres copias son independientes y honestas; las pones lado a lado y ves el eje real. Es un movimiento de código, no una negociación con el sistema.
  • Lo que conservas: la información sobre en qué se diferencian los casos.

Camino B — abstraer mal una vez.

  • Costo de escribir: una interfaz y tres implementaciones. Más que el camino A.
  • Costo de cargar: un concepto nuevo del proyecto, más saltos, más archivos, más superficie de bug.
  • Riesgo: que llegue un caso que no encaje. Y va a llegar, porque elegiste el eje con información incompleta.
  • Costo de arreglar más tarde: crece con el tiempo, en proporción al número de sitios de llamada que se apoyaron en el eje equivocado.
  • Lo que pierdes: la información sobre las diferencias, que quedó codificada como parámetros y banderas.

Y los números de Boletia, que son los que dan la lección:

Sin abstraerAbstraído mal
Lugares que hay que tocar para corregir el diseño3 funciones duplicadas14 sitios que conocen la convención "pending:"
¿El costo crece con el tiempo?No, es el número de copiasSí, crece con cada uso nuevo
¿Alguien puede tocarlo sin miedo?Sí, cada copia es localNo, es la viga detrás del muro

De ahí sale la frase, con su condición explícita:

Duplicar dos veces suele ser más barato que abstraer mal una vez, siempre que el costo de una divergencia entre copias sea cosmético y no catastrófico.

Esa condición final no es un adorno. Si la duplicación está en cálculo de dinero, en permisos o en validación de seguridad, una divergencia deja de ser cosmética y la cuenta cambia; ahí unifica desde el segundo caso, como vimos en la lección 3.

Cómo se sale de una abstracción equivocada

El procedimiento tiene cinco pasos y el segundo es el que cuesta aceptar. Este es el mismo que vas a ejecutar en el proyecto.

Paso 1 — Deja de agregarle cosas. Cuando reconozcas las señales, la primera acción no es arreglar: es no empeorar. Nada de un parámetro más "mientras tanto". Cada vuelta del ciclo sube el costo del arreglo.

Paso 2 — Vuelve a duplicar. Toma cada implementación y mete el código de la abstracción dentro de ella. Que cada caso vuelva a tener su versión completa, aunque quede repetido. Sí, esto se siente como retroceder.

No lo es, y esta es la idea más importante de la lección: la abstracción equivocada destruyó información, y duplicar es cómo la recuperas. Mientras las tres implementaciones estén enredadas en un molde común, no puedes ver en qué eran distintas, porque las diferencias están disfrazadas de parámetros. Separarlas es un acto de diagnóstico.

Paso 3 — Limpia cada copia. Ahora que cada caso está solo, borra de él todo lo que no le aplica. El de correo pierde el max_length que nunca usó. El de SMS pierde el plain_text. El de push pierde el include_links. Cada función se encoge y —esto sorprende siempre— cada una queda más corta y más clara que su implementación original dentro de la abstracción.

Paso 4 — Mira las copias limpias. Ahora sí tienes lo que necesitabas al principio: tres casos completos, honestos e independientes. Ponlos lado a lado y pregunta qué comparten de verdad.

Paso 5 — Decide. Hay tres salidas posibles y las tres son válidas:

  • Comparten un eje claro → abstrae de nuevo, sobre ese eje, ahora con la información completa.
  • Comparten poco → déjalas duplicadas. Eso no es rendirse: es la respuesta correcta cuando el parecido era superficial. En el caso de los mensajes de confirmación, esta es la salida buena.
  • Comparten algo pequeño → extrae solo eso. Muchas veces resulta ser una función auxiliar de dos líneas, no una jerarquía. format_money es exactamente eso.

Un consejo práctico sobre los pasos 2 y 3: hazlos con la red puesta. Antes de empezar, escribe un test que fije el comportamiento observable —qué texto sale para cada canal, con una orden de ejemplo— y córrelo después de cada paso. Ese test no prueba el diseño; prueba que no cambiaste el resultado mientras lo mueves. Es el mismo tipo de red que vas a usar en el proyecto.

Errores comunes

Tratar "no te repitas" como una ley sobre el texto (conceptual). Qué pasa: alguien encuentra dos bloques parecidos y los unifica reflexivamente, porque repetirse está mal. Meses después esa unificación tiene cuatro parámetros para distinguir casos que nunca debieron estar juntos. Por qué pasa: el principio se enseña casi siempre en su versión corta —"no repitas código"— y esa versión es una simplificación de la formulación original, que habla de que cada pieza de conocimiento tenga una representación única y autoritativa en el sistema. Conocimiento, no texto. Cómo detectarlo: si al unificar no puedes decir qué regla de negocio quedó en un solo lugar, no estás aplicando el principio: estás borrando caracteres. Cómo corregirlo: usa la prueba de la lección 3 —si esto cambia, ¿tienen que cambiar los dos a la vez, siempre?—. Y ten a mano la formulación completa del principio, que es tu mejor argumento en una discusión: nadie puede acusarte de violar "no te repitas" si citas su definición original.

Agregar un parámetro en vez de rediseñar, cada vez (de ejecución). Qué pasa: llega un caso que no encaja y se le hace espacio con un parámetro opcional. La decisión es correcta esa vez; el problema es que también lo es la siguiente, y la siguiente. Cada vuelta es defendible y el resultado acumulado no. Por qué pasa: es una asimetría estructural entre lo local y lo global. El costo de rediseñar se paga hoy y completo; el costo de deformar se paga mañana y repartido. Ningún proceso de revisión pensado para evaluar cambios de uno en uno puede detectar esto, porque el problema no está en ningún cambio: está en la serie. Cómo detectarlo: mira el historial de la interfaz, no el cambio que tienes delante. Si en los últimos seis meses la firma creció tres veces y ninguna funcionalidad nueva se agregó a la abstracción en sí, estás en el ciclo. Cómo corregirlo: haz explícito el umbral antes de necesitarlo. Un acuerdo de equipo del tipo "al tercer parámetro opcional, se revisa el diseño" funciona sorprendentemente bien, porque convierte una serie invisible en un evento visible.

No atreverse a des-abstraer porque parece un retroceso (de actitud). Qué pasa: alguien reconoce perfectamente que la abstracción está mal, y aun así no ejecuta el paso 2, porque volver a duplicar se siente como deshacer trabajo, como admitir una derrota, o como algo que un revisor va a rechazar. En vez de eso, intenta arreglar la abstracción desde adentro —agregando otra capa, otro nivel de herencia, un método plantilla más— y el resultado es peor. Por qué pasa: culturalmente, agregar estructura se lee como progreso y quitarla como regresión, aunque el resultado sea mejor. Además, un cambio que borra una clase se ve raro en una revisión. Cómo detectarlo: si te encuentras diseñando una abstracción para arreglar otra abstracción, párate. Cómo corregirlo: replantea el movimiento en voz alta y en el texto del cambio. Duplicar aquí no es retroceder: es recuperar información que la abstracción había destruido, y es un paso intermedio de un procedimiento con cinco pasos. Explicado así, casi nadie se opone. El módulo 8 tiene una lección entera sobre cómo justificar un cambio por su porqué y no por su nombre; esta es una de sus aplicaciones más útiles.

Ejercicios

Ejercicio 1 — Cuenta las deformaciones y su causa. Vuelve a la versión degradada de MessageBuilder. Para cada uno de sus seis miembros —los tres parámetros y los tres métodos extra— di: (a) qué implementación lo usa de verdad; (b) qué caso nuevo lo causó; y (c) qué señal de la tabla es. Después contesta: ¿cuántos de los seis miembros son de verdad comunes a los tres canales?

Ver solución
MiembroQuién lo usaLo causóSeñal
max_lengthSMS (160) y push (120)El límite de caracteres del SMSParámetro opcional
include_linksEmail (True), SMS (False)Que los operadores recorten los enlaces del SMSBandera booleana
plain_textSolo emailClientes de correo que no muestran HTMLBandera booleana
title()Solo pushQue push tenga título aparteMétodo que devuelve None en la mayoría
payload_extras()Solo pushEl enlace profundo de la appMétodo que devuelve {} en la mayoría
supports_html()Solo emailLo mismo que plain_textMétodo que devuelve False en la mayoría

Miembros de verdad comunes a los tres: cero. Ni uno solo de los seis le sirve a las tres implementaciones. Lo único común es el método build y el hecho de recibir una orden.

Dos observaciones que valen más que la tabla. La primera: plain_text y supports_html() son la misma información expresada dos veces, una como parámetro de entrada y otra como método de consulta. Por eso el llamador escribe plain_text=not builder.supports_html(), que es una de esas líneas donde el código está preguntándole a un objeto algo para devolvérselo inmediatamente. Cuando veas esa forma, casi siempre hay una responsabilidad puesta en el lugar equivocado.

La segunda: el ciclo de degradación se puede reconstruir del historial. Primero llegó el SMS con su límite —ahí nació max_length—, después el problema de los enlaces —include_links—, después los clientes de correo sin HTML —plain_text y supports_html—, y al final push, que trajo dos métodos de un golpe. Cuatro vueltas, cuatro decisiones razonables, un resultado sin defensa posible.

Por qué funciona: la tabla convierte "esta interfaz está inflada" en un inventario. Y el inventario es lo que puedes llevar a una discusión de equipo: cero miembros comunes de seis es un argumento que no se discute.

Ejercicio 2 — Haz la cuenta de los dos caminos. Boletia necesita validar cupones de descuento. Hoy hay dos tipos: porcentaje fijo y monto fijo. Producto menciona que "en algún momento" habrá cupones por evento y cupones de un solo uso. Un compañero propone crear ahora una interfaz Coupon con apply(order) -> float. Estima los dos caminos —abstraer ahora contra duplicar y esperar— usando las cuatro variables de la lección 2, y da tu recomendación.

Ver solución

Camino "abstraer ahora":

  • Construir: bajo, una interfaz de un método y dos clases.
  • Cargar: moderado. Un concepto nuevo, dos archivos, un salto. No es grave.
  • Quitar: moderado, y crece con cada sitio de llamada.
  • Beneficio: incierto, y aquí está el problema. La firma apply(order) -> float supone que un cupón es siempre una transformación del total. ¿Un cupón "de un solo uso" cabe ahí? No: eso no es un cálculo, es una regla de elegibilidad con estado —hay que saber si ya se usó y marcarlo—. ¿Un cupón "por evento" cabe? Tampoco del todo: eso es una condición previa, no una transformación.

Es decir: los dos casos futuros que producto mencionó son precisamente los que no encajan en la firma propuesta. Con los dos casos actuales encaja perfecto, porque los dos actuales son transformaciones puras del total. Es el mismo error del PaymentProvider de la lección 3, en otro rincón.

Camino "duplicar y esperar": dos funciones, apply_percentage_coupon(order, coupon) y apply_fixed_coupon(order, coupon), en un archivo pricing/coupons.py, cada una de cuatro o cinco líneas. La duplicación real entre ellas es la validación de "el cupón no ha vencido", que se extrae a una función auxiliar de dos líneas y ya.

Recomendación: duplicar y esperar. Y —esto es lo que convierte la recomendación en criterio— con una condición explícita: "cuando exista el tercer tipo de cupón implementado y en uso, rediseñamos con los tres delante. Si el tercero resulta ser el de un solo uso, ya sabemos que la abstracción va a necesitar separar el cálculo de la elegibilidad, igual que en PricingRule."

Fíjate en el detalle que hace fuerte esta respuesta: no dice "no abstraigas". Dice "todavía no, y aquí está lo que va a decidirlo". Y de paso deja escrito un aprendizaje de otro rincón del sistema, que es exactamente lo que hace un equipo que acumula criterio en vez de repetir errores.

Por qué funciona: este ejercicio te enfrenta al caso más difícil, que es cuando hay información sobre el futuro. Producto mencionó dos casos más. La tentación de abstraer es enorme. Y resulta que esos dos casos futuros son la mejor evidencia de que la interfaz propuesta está en el eje equivocado. Saber leer esa evidencia es la habilidad completa de este módulo.

Ejercicio 3 — Ejecuta el procedimiento de salida. Aplica los cinco pasos a la abstracción MessageBuilder de esta lección. Escribe, para cada paso, qué harías concretamente. En el paso 5 justifica cuál de las tres salidas eliges y por qué.

Ver solución

Paso 1 — Deja de agregarle cosas. Concretamente: si hay un requisito en cola —digamos, un cuarto canal de WhatsApp— no se agrega a la interfaz. Se escribe aparte y se hace el rediseño primero. Además, se deja una nota en el archivo diciendo que la interfaz está en revisión y que no se le agreguen miembros.

Paso 2 — Vuelve a duplicar. Toma EmailMessageBuilder.build() y mete adentro el código que hoy hereda de MessageBuilder, resolviendo los parámetros con los valores que el llamador le pasa realmente: max_length=None, include_links=True, plain_text según corresponda. Lo mismo con SMS y push. Al final tienes tres funciones completas, repetidas, y todavía feas.

Paso 3 — Limpia cada copia. Ahora borra de cada una lo que su caso no usa. En email desaparecen max_length y el manejo del truncado. En SMS desaparecen plain_text y el HTML. En push desaparece include_links. Cada función se encoge a cuatro o cinco líneas. Este es el paso donde el resultado empieza a verse mejor que el original.

Paso 4 — Mira las copias limpias. Lo que comparten los tres, mirándolos juntos: (a) reciben una orden; (b) llaman a format_money(order.total); (c) mencionan el número de orden; (d) construyen algún tipo de saludo o encabezado. Lo que no comparten: el tipo de retorno (dos strings y un diccionario), las restricciones de longitud, el marcado, y la presencia de enlaces.

Paso 5 — Decide: la tercera salida, "extrae solo lo pequeño".

La abstracción completa no se justifica: el tipo de retorno de push es distinto, y una interfaz que tenga que unificar str con dict va a necesitar una unión o un envoltorio, que es más ceremonia que la que se quería ahorrar. Y dejarlas totalmente duplicadas tampoco es óptimo, porque hay una pieza que sí es conocimiento compartido: cómo se muestra un monto de dinero al cliente. Si mañana Boletia cambia el formato de moneda, tiene que cambiar en los tres.

Así que el resultado es: tres funciones independientes en el archivo de su canal, y una función auxiliar format_money —que ya existía— usada por las tres. Cero clases nuevas, cero conceptos nuevos, y el conocimiento compartido en un solo lugar.

La justificación en una frase, del tipo que vas a escribir en el proyecto: "los tres mensajes se parecían en la superficie y se diferencian en todo lo que importa —tipo de retorno, límites y marcado—. Lo único que de verdad comparten es el formato del dinero, que ya estaba extraído. La interfaz común costaba seis miembros de los cuales ninguno era común a los tres."

Por qué funciona: la salida "no re-abstraigas" es la que cuesta más elegir y la que más veces es correcta. Este ejercicio te obliga a llegar hasta el paso 5 con la información completa en la mano y a comprobar que la respuesta honesta era la más simple.

Resumen y siguiente paso

En esta lección comparaste de frente los dos problemas que se confunden todo el tiempo. La duplicación es la mancha: visible, local, reversible, con un costo lineal que avisa a tiempo y un peor caso acotado. La abstracción equivocada es la viga detrás del muro: invisible para las herramientas y para las métricas, sistémica, con un costo que crece con cada uso nuevo, y —sobre todo— heredable.

Viste los seis mecanismos por los que se hereda: las métricas la premian, la cerca de Chesterton la protege, el camino de menor resistencia siempre es agregar, cada uso nuevo la fija más, nadie es dueño, y el costo se atribuye al dominio en vez de al diseño.

Recorriste el ciclo de degradación en los mensajes de confirmación de Boletia: una interfaz impecable con dos canales que, después de cuatro rondas de ajustes razonables, terminó con seis miembros de los cuales ninguno era común a los tres canales, y con el condicional por canal de vuelta en el llamador, ahora más complicado que el original. Y aprendiste las señales de cada vuelta, empezando por la más barata de detectar: cada bandera booleana en una firma es un condicional que le pasaste al llamador.

Hiciste la cuenta honesta —tres funciones duplicadas contra catorce sitios acoplados— y viste la frase con su condición: duplicar dos veces suele ser más barato que abstraer mal una vez, siempre que el costo de una divergencia sea cosmético y no catastrófico.

Y te llevas el procedimiento de salida, cuyo segundo paso es el que cuesta aceptar y el que hace todo el trabajo: volver a duplicar no es retroceder, es recuperar la información que la abstracción había destruido.

Antes de avanzar deberías poder: explicar por qué la duplicación conserva información y la abstracción prematura la destruye; reconocer al menos cuatro señales de degradación en una firma; y ejecutar los cinco pasos de salida sobre un caso pequeño.

Queda una pieza. Todo lo que vimos aquí es sobre el daño: qué forma tiene y cómo se repara. No hablamos de la motivación: por qué gente competente y bienintencionada construye cosas para un futuro que no llegó. Esa motivación tiene nombre —construir para lo que imaginaste— y tiene un antídoto que se enuncia en cuatro letras y se malinterpreta casi siempre. La lección 5 se ocupa de YAGNI, y sobre todo de su matiz: la línea entre la preparación barata y reversible, que sí conviene, y la infraestructura especulativa, que no.

Recursos

  • The Wrong Abstraction (Sandi Metz) — el texto del que sale el ciclo de degradación y la frase "duplication is far cheaper than the wrong abstraction". Cuatro párrafos; léelo entero.
  • All the Little Things (Sandi Metz, RailsConf 2014) — la charla donde ejecuta en vivo el procedimiento de salida sobre un ejemplo real. Ver a alguien duplicar a propósito, con explicación, vale más que diez artículos.
  • Don't Repeat Yourself, en The Pragmatic Programmer — la formulación original del principio: cada pieza de conocimiento debe tener una representación única, inequívoca y autoritativa. Conviene tenerla a mano; es un argumento sólido en cualquier discusión.
  • Chesterton's Fence — la parábola completa y su aplicación a la toma de decisiones. Úsala con su versión completa: no la quites hasta saber por qué está, y averígualo.