computación científica e ingeniería Múltiples backends numéricos: CPU, GPU, TPU, clusters distribuidos, hardware especializado (JAX, CuPy, PyTorch). Múltiples fuentes de datos: experimentos instrumentados, simulaciones previas, bases de datos externas, APIs remotas. Múltiples esquemas de validación: unidades físicas, condiciones de frontera, restricciones de conservación, tolerancias numéricas. Múltiples consumidores: scripts de análisis, notebooks interactivos, pipelines automatizados, servicios web. Esta heterogeneidad no se gestiona bien con funciones sueltas. Se gestiona con abstracciones explícitas que capturen las invariantes del dominio y las hagan cumplir mecánicamente. Un objeto CampoElectromagnetico que conoce sus unidades, su malla de discretización y sus condiciones de frontera es categóricamente distinto de un numpy.ndarray anónimo al que el usuario debe recordar aplicarle la transformación correcta. Considere el siguiente contraste. En un estilo puramente procedural, un solver de elementos finitos podría verse así: 1 2 3 4 5 6 def resolver ( malla , material , fuente , bc_izq , bc_der , tol , max_iter ) : K = ensamblar_rigidez ( malla , material ) F = ensamblar_fuerza ( malla , fuente ) K , F = aplicar_dirichlet (K , F , malla , bc_izq , bc_der ) u = gradiente_conjugado (K , F , tol , max_iter ) return u Listing 1: Solver procedural: toda la información dispersa en argumentos. El código funciona, pero cada parámetro es un contrato implícito. Agregar una nueva condición de frontera requiere modificar la firma de resolver, lo que rompe la compatibilidad con todos sus llamadores. Cambiar el material de isótropo a anisótropo requiere modificar ensamblar_rigidez, que a su vez depende del formato exacto del argumento material. La complejidad accidental crece linealmente con cada nueva capacidad. En estilo orientado a objetos, la misma lógica se distribuye entre entidades autónomas: 1 2 3 4 5 6 class Problema : def __init__ ( self , malla , material , fuente , condiciones ) : self . malla = malla self . material = material self . fuente = fuente self . condiciones = condiciones # lista de CondicionFrontera 7 8 9 10 11 class Solver : def __init__ ( self , tol : float = 1e -8 , max_iter : int = 1000) : self . tol = tol self . max_iter = max_iter 12 13 14 15 16 17 18 def resolver ( self , problema : Problema ) -> " ndarray " : K = problema . malla . rigidez ( problema . material ) F = problema . malla . fuerza ( problema . fuente ) for bc in problema . condiciones : bc . aplicar (K , F ) return self . _gradiente_conjugado (K , F ) Listing 2: Solver orientado a objetos: cada entidad conoce su contrato. Ahora añadir una condición de frontera periódica es extensión (una nueva subclase de CondicionFrontera), no modificación. Cambiar el material es cambiar el objeto Material pasado al constructor, sin tocar el Abraham Zamudio 8