Ilustración conceptual de un dispositivo modular con piezas de colores junto a muestras de puertas y una maqueta arquitectónica.

Composición y herencia: reutilizar comportamiento sin acoplarlo todo

La primera fuente es una lista en memoria. La segunda llega como JSON. Después aparece la tentación de crear un validador distinto para cada origen, porque ambos necesitan leer datos y alguien ha descubierto que las clases pueden heredar. Es comprensible: estrenar una herramienta produce unas ganas de utilizarla que el problema no siempre comparte. Antes de organizar un árbol genealógico de validadores, vamos a separar qué cambia realmente.

La regla será siempre la misma: devolver los identificadores cuyos códigos estén vacíos. Cambiaremos la lectura sin modificar esa regla ni crear una subclase. Lo haremos con Python y C#, usando datos didácticos equivalentes y comprobando también los fallos. La composición que nos interesa aquí consiste en montar un validador con dos piezas que recibe desde fuera: una función que obtiene los registros y otra que los revisa.

Tabla de contenidos

La fuente y la regla cambian por motivos distintos

Una lectura sabe cómo conseguir los datos y convertirlos a una forma acordada. En este artículo esa forma será una secuencia de parejas de textos: identificador y código. El lector de JSON elegirá los campos id y codigo y preparará esas parejas. La regla no tendrá que saber cómo se llamaban los campos del documento ni interpretar su sintaxis. El lector en memoria entregará directamente la misma forma, sin pasar por JSON.

La regla sabe qué condición estamos comprobando: código igual a cadena vacía. No necesita abrir archivos ni gestionar una conexión para decidirlo. Si mañana cambia la fuente, esa comparación sigue significando lo mismo; si mañana decidimos revisar otra condición, no debería ser necesario reescribir el lector. Esta independencia tiene un beneficio comprobable: podemos utilizar la misma regla con dos lectores y comprobarla sin depender de un servicio externo.

Heredar permite definir una clase a partir de otra, reutilizando o especializando su comportamiento. La documentación de herencia de C# explica ese mecanismo; el tutorial de clases de Python describe también su aplicación en ese lenguaje. La dificultad de diseño aparece cuando la relación elegida une decisiones que deberían cambiar por separado. Un lector JSON no es por ese motivo un tipo especial de regla de validación: colabora con ella.

Si creas una subclase por cada combinación de fuente, regla y destino del informe, el número de nombres puede crecer bastante antes que la utilidad. No significa que toda herencia esté mal: significa que compartir unas líneas no basta para justificar la relación. En nuestro caso elegiremos piezas intercambiables y dejaremos la jerarquía sin construir. Es una decisión pequeña que se puede contrastar cambiando una pieza, sin convocar una reunión sobre patrones de diseño.

Python: montar un validador con dos funciones

Guarda este programa completo como validacion_compuesta.py. Utiliza Python 3.10 o posterior y únicamente su biblioteca estándar. Validador conserva dos funciones y coordina su llamada. leer_json interpreta un texto, comprueba que contiene una lista de registros con los dos campos de texto necesarios y devuelve parejas. Los campos adicionales no participan en esta regla.

				
					import json


class Validador:
    def __init__(self, leer, regla):
        if not callable(leer) or not callable(regla):
            raise ValueError("Lector y regla deben ser funciones invocables")
        self._leer = leer
        self._regla = regla

    def ejecutar(self):
        filas = self._leer()
        return self._regla(filas)


def leer_memoria():
    return (("P01", ""), ("P02", "D02"))


def leer_json(texto):
    datos = json.loads(texto)
    if not isinstance(datos, list):
        raise ValueError("Lista de registros requerida")
    filas = []
    for dato in datos:
        if (not isinstance(dato, dict)
                or not isinstance(dato.get("id"), str)
                or not isinstance(dato.get("codigo"), str)):
            raise ValueError("id y codigo deben ser textos")
        filas.append((dato["id"], dato["codigo"]))
    return tuple(filas)


def sin_codigo(filas):
    return [identificador for identificador, codigo in filas if codigo == ""]


texto = '[{"id":"P01","codigo":""},{"id":"P02","codigo":"D02"}]'
lectores = [("memoria", leer_memoria), ("json", lambda: leer_json(texto))]
for nombre, lector in lectores:
    validador = Validador(lector, sin_codigo)
    print(f"{nombre} | {', '.join(validador.ejecutar())}")
				
			

Ejecuta desde la carpeta del archivo:

				
					python validacion_compuesta.py
				
			

La salida será memoria | P01 y json | P01. Se ha utilizado exactamente la misma función sin_codigo con dos lecturas. lambda prepara una función sin argumentos que, al ser llamada, utiliza leer_json con ese texto. Por eso pasamos leer_memoria sin paréntesis: queremos entregar una operación que el validador ejecutará después. Entregar su resultado en lugar de la función cambiaría el contrato y el constructor lo rechazaría.

No hemos creado un lector de archivos: el JSON está dentro del programa para poder ejecutar el ejemplo sin preparar una entrega externa. Tampoco hemos comprobado que los identificadores existan en un modelo o que los códigos pertenezcan a un catálogo. Solo exigimos los dos textos que necesita la regla. La biblioteca json de Python se ocupa de interpretar la sintaxis; el pequeño adaptador posterior decide qué forma de datos acepta esta aplicación.

La clase coordina, pero no tiene una necesidad mística de existir. Para ejecutar una única revisión podría bastar una función que recibiera lector y regla. Aquí la mantenemos como una configuración reutilizable: puedes conservar el mismo validador y ejecutarlo más de una vez. Si no necesitas ese objeto, la composición sigue siendo válida con funciones. El criterio del artículo sobre clases y objetos sigue aplicándose aunque acabemos de aprender otra palabra nueva.

C#: recibir operaciones mediante Func

En .NET 8 crea el proyecto:

				
					dotnet new console --framework net8.0 -n ValidacionCompuesta
				
			

Sustituye ValidacionCompuesta/Program.cs por este programa. Func expresa aquí una operación con resultado: el lector no recibe argumentos y devuelve un array de parejas; la regla recibe esas parejas y devuelve identificadores. El compilador comprueba esas firmas. No comprueba que un lector haya elegido los elementos correctos de un proyecto; esa promesa sigue necesitando pruebas.

				
					using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;

static (string Id, string Codigo)[] LeerMemoria() =>
    [("P01", ""), ("P02", "D02")];

static (string Id, string Codigo)[] LeerJson(string texto)
{
    using var documento = JsonDocument.Parse(texto);
    var filas = new List<(string Id, string Codigo)>();
    foreach (var dato in documento.RootElement.EnumerateArray())
    {
        string id = dato.GetProperty("id").GetString()
            ?? throw new ArgumentException("id debe ser texto");
        string codigo = dato.GetProperty("codigo").GetString()
            ?? throw new ArgumentException("codigo debe ser texto");
        filas.Add((id, codigo));
    }
    return filas.ToArray();
}

static string[] SinCodigo((string Id, string Codigo)[] filas) =>
    filas.Where(f => f.Codigo == "").Select(f => f.Id).ToArray();

string texto = "[{\"id\":\"P01\",\"codigo\":\"\"},{\"id\":\"P02\",\"codigo\":\"D02\"}]";
(string Nombre, Func<(string Id, string Codigo)[]> Leer)[] lectores =
    [("memoria", LeerMemoria), ("json", () => LeerJson(texto))];
foreach (var (nombre, lector) in lectores)
{
    var validador = new Validador(lector, SinCodigo);
    Console.WriteLine($"{nombre} | {string.Join(", ", validador.Ejecutar())}");
}

sealed class Validador
{
    private readonly Func<(string Id, string Codigo)[]> leer;
    private readonly Func<(string Id, string Codigo)[], string[]> regla;

    public Validador(Func<(string Id, string Codigo)[]> leer,
        Func<(string Id, string Codigo)[], string[]> regla)
    {
        this.leer = leer ?? throw new ArgumentNullException(nameof(leer));
        this.regla = regla ?? throw new ArgumentNullException(nameof(regla));
    }

    public string[] Ejecutar()
    {
        var filas = leer();
        return regla(filas);
    }
}
				
			

Ejecuta:

				
					dotnet run --project ValidacionCompuesta
				
			

Las dos líneas coinciden con Python. El lector prepara textos y devuelve un array independiente del documento JSON antes de que termine su ámbito de uso. El validador no conserva un JsonElement que dependa de un documento ya liberado. Esa diferencia es importante al adaptar una biblioteca externa a tus datos: no basta con que la forma parezca correcta; también debe seguir disponible durante el tiempo en que la utiliza la regla.

Las funciones se reciben desde fuera, así que el validador no incluye un condicional que conozca todos los tipos de fuente. Añadir un tercer lector que cumpla la misma firma no exige modificar Ejecutar. Eso no elimina el trabajo del lector nuevo: necesitará interpretar sus datos, comunicar fallos y demostrar que entrega el mismo significado. La composición reduce una dependencia concreta; no fabrica compatibilidad a partir de dos funciones con nombres prometedores.

La herencia sigue teniendo un lugar, pero necesita un contrato

Una clase derivada puede ser adecuada cuando representa una especialización que mantiene lo que espera quien utiliza la clase base. También hay bibliotecas y marcos que piden heredar de un tipo concreto para proporcionar un comportamiento. En esos casos no se resuelve el diseño prohibiendo la palabra class: se entiende qué contrato debe conservar la especialización y qué puntos permite ampliar el marco.

El problema de nuestro validador sería heredar solo para conseguir acceso a una lectura común y terminar haciendo que la regla dependa de detalles de esa clase base. Si la base cambia cuándo lee, qué guarda o qué método llama primero, las subclases pueden verse afectadas aunque su condición de validación no haya cambiado. Esa dependencia puede ser aceptable en una jerarquía pequeña y deliberada; es mucho menos agradable descubrirla cuando has construido una clase por cada formato y cada comprobación.

Con la composición también hay dependencias, solo que aquí son más estrechas y visibles. El lector debe devolver parejas de textos y la regla debe aceptar esas parejas. Si uno cambia su contrato, la colaboración puede romperse. La ventaja no es la ausencia de acuerdos, sino poder localizar el acuerdo que cambia. No necesitamos que el validador conozca el documento JSON completo, los nombres de sus campos ni cómo se ha obtenido el texto.

Si una dependencia necesita varias operaciones relacionadas, estado propio o una política de vida más compleja, puede merecer una interfaz o un objeto colaborador en lugar de una función. No lo añadiremos a este ejemplo, que solo tiene una operación de lectura. Diseñar para una complejidad que ya existe suele resultar más convincente que preparar veinte extensiones para el día en que el informe decida abrir una sucursal.

Cambiar una pieza tiene que dejar pruebas reconocibles

La primera comprobación es comparar los resultados de las dos fuentes: deben devolver P01 con los datos originales. Después cambia ambas entradas para que todos los códigos estén informados; ambas deben devolver una colección vacía. Prueba una entrada vacía y otra con varios códigos vacíos. La regla conserva el orden de llegada y los identificadores repetidos; no ordena ni deduplica, porque no se lo hemos pedido.

Para comprobar que fuente y regla no están pegadas, utiliza otra regla que seleccione códigos informados y conserva ambos lectores. Con la entrada original debe devolver P02 en los dos casos, sin tocar Validador. También puedes introducir un lector de prueba que registre cuántas veces se llama y una regla que registre qué recibe: cada ejecución debe leer una vez y aplicar después la regla a esos datos. Esa observación demuestra el recorrido que hemos escrito, sin confundirlo con una integración real.

Rompe a continuación la lectura: JSON mal formado, un campo ausente o un código que sea un número. El ejemplo debe fallar antes de aplicar la regla. No capturamos esos errores para devolver una colección vacía, porque «no he podido leer» y «he leído y no encontré códigos vacíos» son resultados diferentes. Si la regla lanza un error, también debe llegar a quien llama. Más adelante una aplicación podrá mostrarlo o registrarlo; ocultarlo aquí solo produciría un informe tranquilizador con una relación bastante flexible con lo ocurrido.

Ese es el cambio que quería conseguir: sustituir una lectura sin reescribir la condición, y poder señalar en qué lado ha fallado una prueba. La herencia queda disponible para relaciones que la justifiquen. Antes de elegirla por comodidad, prueba a nombrar las piezas que cambian por separado y el dato que intercambian. Si ahora tu validador abre archivos, interpreta una API, decide reglas y formatea un informe en el mismo recorrido, cuenta qué parte te cuesta más probar: probablemente esa sea una frontera más útil que el siguiente nombre de subclase.

Artículos relacionados

Respuestas

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *