La programación imperativa describe cómo obtener un resultado mediante instrucciones, mientras que la declarativa expresa qué resultado queremos y deja los pasos concretos a otra capa. Son dos enfoques que conviven constantemente en el software.
Cuando empezamos a programar, casi todos aprendemos escribiendo secuencias de instrucciones que modifican la memoria del ordenador paso a paso. Es natural, porque se parece al funcionamiento del hardware basado en la arquitectura de von Neumann.
A medida que el software crece, gestionar cada detalle del cómo puede ser propenso a errores. Esto no significa abandonar lo imperativo, sino usar abstracciones declarativas cuando expresan mejor la intención.
Hoy vamos a diseccionar estos dos enfoques con una carga técnica mayor, analizando cómo gestionan el estado, el flujo de control y los efectos secundarios.
Programación imperativa: el control del flujo
La programación Imperativa se centra en describir cómo alcanzar un resultado. El programador dicta paso a paso los cambios de estado necesarios para computar la solución.
Técnicamente, se basa en tres pilares:
- Sentencias: comandos que ejecutan una acción.
- Mutación de estado: variables que cambian de valor a lo largo del tiempo.
- Estructuras de control explícitas: bucles
for,whiley condicionalesif/else.
Es el paradigma de C (original), C++ (clásico), Java (pre-8) y la base de C#.
Ejemplo imperativo en C#
Supongamos que queremos filtrar una lista de números para obtener solo los pares y luego elevarlos al cuadrado.
List<int> numeros = new List<int> { 1, 2, 3, 4, 5, 6 };
List<int> resultados = new List<int>(); // 1. Estado mutable externo
// 2. Control de flujo explícito (Cómo iterar)
for (int i = 0; i < numeros.Count; i++)
{
// 3. Control de flujo condicional (Cómo filtrar)
if (numeros[i] % 2 == 0)
{
int cuadrado = numeros[i] * numeros[i];
// 4. Efecto secundario: Mutación de la lista de resultados
resultados.Add(cuadrado);
}
}
// resultados: { 4, 16, 36 }
El código es directo, pero contiene más detalles mecánicos: gestionamos el índice i, la colección resultados y la condición de parada. La intención («pares al cuadrado») queda mezclada con la «fontanería» del bucle. Además, esta versión no se puede paralelizar sin cambiar cómo se escribe en resultados.
Programación declarativa: la abstracción lógica
La programación Declarativa se centra en describir qué es lo que queremos obtener, delegando el control de flujo al lenguaje o framework subyacente.
En lugar de especificar el recorrido, componemos expresiones que describen la transformación. Algunos estilos declarativos, como la programación funcional, favorecen además:
- Inmutabilidad: crear nuevos valores en lugar de modificar los existentes.
- Funciones de orden superior: funciones que reciben o devuelven otras funciones, como
Map,FilteroReduce. - Control de los efectos secundarios: mantener separados los cálculos puros y las operaciones que modifican el exterior.
Estas propiedades no definen toda programación declarativa. SQL, por ejemplo, es declarativo aunque una consulta pueda terminar modificando datos.
Ejemplo declarativo en C# (LINQ)
Veamos el mismo problema resuelto de forma declarativa usando LINQ (Language Integrated Query).
List<int> numeros = new List<int> { 1, 2, 3, 4, 5, 6 };
// "Dime QUÉ quieres":
// Quieres números DONDE sean pares, SELECCIONANDO su cuadrado.
var resultados = numeros
.Where(n => n % 2 == 0) // Filtro (Declarativo)
.Select(n => n * n) // Transformación (Proyección)
.ToList(); // Materialización
Aquí no hay bucles for, ni índices i, ni if explícitos.
Estamos construyendo una pipeline de datos.
Whereno recorre la colección inmediatamente; construye una consulta con evaluación diferida.- El código muestra la intención sin exponer el índice ni el bucle.
- Podemos evaluar si PLINQ (
AsParallel) aporta rendimiento, aunque paralelizar no es gratis y exige que las operaciones sean seguras y tengan suficiente carga para compensar el coste.
Comparativa técnica
¿Por qué el mundo se mueve hacia lo declarativo?
El estado compartido mutable es una fuente frecuente de errores, especialmente cuando existe concurrencia.
- Imperativo: “Tengo una variable global
x. La función A la lee. La función B la cambia. La función C la borra”. Si B se ejecuta antes que A por un problema de hilos (Race Condition), el programa explota. - Declarativo: Los datos fluyen a través de funciones puras.
Input -> Función -> Output. El input original no se toca. Esto elimina clases enteras de errores de concurrencia.
En el código imperativo, para entender qué hace un algoritmo, debes “ejecutarlo mentalmente” (“vale, i empieza en 0, entra al if, suma 1…”).
En el código declarativo, lees la intención. Esto puede reducir la complejidad visible, aunque los detalles siguen existiendo dentro de la abstracción.
:::
UI declarativa
Donde más fuerte ha pegado este cambio es en el desarrollo de Interfaces de Usuario (Frontend).
En enfoques como WinForms o jQuery es habitual actualizar la UI de forma imperativa. WPF, por su parte, ya incorporó mecanismos declarativos mediante XAML y data binding.
// Imperativo (WinForms / Android clásico)
void OnButtonClick() {
boton.Text = "Cargando..."; // Mutamos el estado de la UI manualmente
label.Visible = true;
}
Hoy en día (React, Flutter, MAUI, SwiftUI), la UI es declarativa. La UI es una función del estado: UI = f(State).
// Declarativo (Concepto tipo React/Blazor)
// Solo definimos cómo se ve la UI basándonos en el estado.
// El framework se encarga de repintar lo que cambie.
<Button Text="@(IsLoading ? "Cargando..." : "Enviar")" />
<Label Visible="@IsLoading" />
¿Ha muerto la programación imperativa?
No. Y es importante entender esto.
La programación declarativa es una abstracción. Por debajo, en las tripas del compilador de C# o del motor de JavaScript, alguien ha tenido que escribir código imperativo para que Where o Select funcionen. La CPU sigue ejecutando instrucciones imperativas en ensamblador.
Además, hay situaciones donde el rendimiento extremo o el control total del hardware requieren un enfoque imperativo (drivers, motores de videojuegos de bajo nivel).
Sin embargo, para el 95% de la lógica de negocio y arquitectura de aplicaciones empresariales, el enfoque declarativo es superior. Nos permite centrarnos en la lógica del dominio y olvidar la fontanería del flujo de control.