Parte 9 — Ingeniería de software políglota · ⏱️ Duración estimada: 90 min · Nivel: Intermedio ✅ Clase construida — 10 implementaciones del núcleo verificadas contra
casos.json.
Ningún programa profesional se construye solo con el código que escribimos: descansa sobre decenas o cientos de dependencias de terceros, y cada una de ellas evoluciona a su propio ritmo. La pregunta central de esta clase es cómo mantener un proyecto estable cuando todo lo que hay debajo se mueve. La respuesta tiene dos piezas que hay que entender a fondo. La primera es el versionado semántico (SemVer): la convención mayor.menor.parche que convierte un simple número en un contrato de compatibilidad entre quien publica una librería y quien la consume. La segunda es el lockfile: el archivo que congela el árbol de dependencias resuelto para que la build sea reproducible, hoy y dentro de dos años, en tu máquina y en la de integración continua.
El ejercicio de esta clase es deliberadamente pequeño —descomponer 1.2.3 en sus tres números— porque el parseo es la operación mínima sobre la que se apoya todo lo demás. Un gestor de paquetes hace exactamente esto miles de veces al resolver un grafo: lee versiones, las compara según las reglas de SemVer y decide cuál instalar. Al implementarlo en diez lenguajes verás que la lógica es trivial; lo difícil, y lo que discute The Pragmatic Programmer de Hunt y Thomas bajo el principio DRY («Don't Repeat Yourself»), es no repetir esa decisión en cada máquina: se toma una vez, se escribe en el lockfile y se reutiliza. El lockfile es la forma más concreta de «no repetirse» sobre el entorno de ejecución.
Al finalizar, podrás:
mayor.menor.parche en sus tres componentes numéricos.^ y ~.| # | Tema | Por qué importa |
|---|---|---|
| 1 | SemVer mayor.menor.parche |
Un número que comunica riesgo de ruptura |
| 2 | Rangos ^ y ~ |
Definen qué actualizaciones aceptas sin revisar |
| 3 | Lockfile | Congela el árbol resuelto: build reproducible |
| 4 | Lockfile por ecosistema | Cada lenguaje tiene el suyo y hay que versionarlo |
Versionado semántico (SemVer). La especificación de SemVer fija que, dada una versión MAYOR.MENOR.PARCHE, se incrementa el mayor ante cambios incompatibles en la API, el menor al añadir funcionalidad de forma retrocompatible, y el parche al corregir errores sin cambiar la interfaz. Lo importante no es la aritmética sino el pacto: cuando un autor sube solo el parche, te está diciendo «puedes actualizar a ciegas». Cuando sube el mayor, te avisa de que algo que usabas dejó de funcionar. SemVer es, en el fondo, documentación ejecutable de la compatibilidad.
Rangos (^, ~). Rara vez fijamos una versión exacta en el manifiesto; declaramos un rango. El cursor (^1.4.2) admite cualquier versión menor o parche mientras el mayor no cambie (>=1.4.2 <2.0.0); la tilde (~1.4.2) es más estricta y suele limitarse a parches (>=1.4.2 <1.5.0). El rango expresa tu tolerancia al cambio automático.
Lockfile. Un manifiesto con rangos es una intención; el lockfile es el resultado. Registra la versión exacta que se resolvió para cada dependencia (directa y transitiva), casi siempre con un hash de integridad. Su razón de ser, en la línea del principio DRY de Hunt y Thomas, es que la resolución del grafo se haga una sola vez y todos —compañeros y servidores de CI— reciban idénticos bytes.
Tu equipo mantiene un servicio en producción y declara en el manifiesto una dependencia como ^1.4.2. Una mañana el build de un compañero recién incorporado falla y el tuyo no: su instalación limpia resolvió 1.7.0 —publicada anoche— mientras tú seguías en 1.4.2. Nada en el manifiesto cambió; cambió lo que había disponible en el registro. Este es el escenario que el lockfile elimina de raíz: al versionarlo junto al código, la instalación de tu compañero habría leído 1.4.2 del lock en lugar de re-resolver el rango. La primera tarea, antes de comparar versiones, es descomponerlas con fiabilidad; eso es lo que implementa el laboratorio.
mayor.menor.parchemayor=<M> menor=<m> parche=<p>Especificación y verificación en casos.json:
| stdin | esperado |
|---|---|
1.2.3 |
mayor=1 menor=2 parche=3 |
0.5.10 |
mayor=0 menor=5 parche=10 |
2.0.0 |
mayor=2 menor=0 parche=0 |
LEER version ; separar por '.' ; ESCRIBIR componentes
Mismo algoritmo, forma idiomática en cada lenguaje. Todas producen la salida de casos.json.
Cada bloque es el archivo real de implementaciones/. El contrato es fijo: leer una línea mayor.menor.parche de stdin y escribir mayor=<M> menor=<m> parche=<p>. Para 1.2.3 la salida es mayor=1 menor=2 parche=3, y para 0.5.10, mayor=0 menor=5 parche=10 —el 10 es la razón por la que hay que convertir a entero: el parche de dos dígitos no encaja en una comparación textual ingenua.
python/main.py · python main.pyimport sys
mayor, menor, parche = sys.stdin.readline().strip().split(".")
print(f"mayor={int(mayor)} menor={int(menor)} parche={int(parche)}")
🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R
La versión de Python condensa todo en dos líneas y merece leerse con calma. split(".") parte la cadena por los puntos y devuelve una lista de tres cadenas; el desempaquetado mayor, menor, parche = ... las asigna en un solo gesto —una construcción que Ramalho, en Fluent Python, presenta como unpacking y considera una de las señas de identidad del código pitónico frente al indexado manual v[0], v[1], v[2]. El int(...) de cada componente no es cosmético: garantiza que 10 se imprima como el número 10 y no arrastre un espacio o un salto de línea. Aquí f"..." interpola directamente el valor evaluado. El resultado es exactamente el esperado de casos.json.
javascript/main.mjs · node main.mjsimport { readFileSync } from "node:fs";
const [mayor, menor, parche] = readFileSync(0, "utf8").trim().split(".").map(Number);
console.log(`mayor=${mayor} menor=${menor} parche=${parche}`);
🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript
typescript/main.ts · pnpm exec tsx main.tsimport { readFileSync } from "node:fs";
const [mayor, menor, parche] = readFileSync(0, "utf8").trim().split(".").map(Number);
console.log(`mayor=${mayor} menor=${menor} parche=${parche}`);
🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript
java/Main.java · java Main.javaimport java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String[] v = br.readLine().trim().split("\\.");
System.out.println("mayor=" + Integer.parseInt(v[0]) + " menor=" + Integer.parseInt(v[1]) + " parche=" + Integer.parseInt(v[2]));
}
}
🧬 El mismo programa en la familia JVM: Kotlin · Scala · Groovy · Clojure
csharp/Program.cs · dotnet runusing System;
string[] v = Console.In.ReadToEnd().Trim().Split('.');
Console.WriteLine($"mayor={int.Parse(v[0])} menor={int.Parse(v[1])} parche={int.Parse(v[2])}");
🧬 El mismo programa en la familia .NET: F# · VB.NET
go/main.go · go run main.gopackage main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
v := strings.Split(strings.TrimSpace(line), ".")
ma, _ := strconv.Atoi(v[0])
me, _ := strconv.Atoi(v[1])
pa, _ := strconv.Atoi(v[2])
fmt.Printf("mayor=%d menor=%d parche=%d\n", ma, me, pa)
}
🧬 El mismo programa en la familia Sistemas: Zig · Nim · D
rust/main.rs · rustc main.rs -o main && ./mainuse std::io::Read;
fn main() {
let mut s = String::new();
std::io::stdin().read_to_string(&mut s).unwrap();
let v: Vec<i64> = s.trim().split('.').map(|x| x.parse().unwrap()).collect();
println!("mayor={} menor={} parche={}", v[0], v[1], v[2]);
}
🧬 El mismo programa en la familia Sistemas: Zig · Nim · D
c/main.c · cc main.c -o main && ./main#include <stdio.h>
int main(void) {
long ma, me, pa;
if (scanf("%ld.%ld.%ld", &ma, &me, &pa) != 3) return 1;
printf("mayor=%ld menor=%ld parche=%ld\n", ma, me, pa);
return 0;
}
🧬 El mismo programa en la familia C / llaves: C++ · Objective-C
El contraste con C es instructivo. Donde Python separa y luego convierte, C hace ambas cosas a la vez: la cadena de formato "%ld.%ld.%ld" de scanf describe la estructura esperada —tres enteros largos con puntos entre ellos— y el parser la aplica en una sola llamada. Fiel al espíritu de Kernighan y Ritchie en The C Programming Language, el código comprueba el valor de retorno (!= 3) y aborta si no leyó los tres campos: en C nada valida por ti, el contrato lo verificas a mano. Rust ocupa un punto intermedio: split('.').map(|x| x.parse().unwrap()) encadena separación y conversión en una expresión funcional, y el unwrap() hace explícito que un formato inválido debe interrumpir el programa —la filosofía de manejo de errores que Klabnik y Nichols describen en The Rust Programming Language, donde ignorar un Result es una decisión visible, no un descuido.
sql/main.sql · sqlite3 :memory: < main.sql-- SQL: separa la versión con funciones de texto (ilustrativo).
WITH v(s) AS (VALUES ('1.2.3'))
SELECT printf('mayor=%d menor=%d parche=%d',
CAST(substr(s, 1, instr(s, '.') - 1) AS INTEGER),
CAST(substr(s, instr(s, '.') + 1, instr(substr(s, instr(s, '.') + 1), '.') - 1) AS INTEGER),
CAST(substr(s, length(s) - instr(reverse(s), '.') + 2) AS INTEGER)) AS resultado
FROM v;
🧬 El mismo programa en la familia Lógica y declarativa: Prolog · Datalog
php/main.php · php main.php<?php
[$ma, $me, $pa] = explode(".", trim(fgets(STDIN)));
echo "mayor=" . (int) $ma . " menor=" . (int) $me . " parche=" . (int) $pa . "\n";
🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R
SQL es declarativo: no lee de stdin como los demás; su implementación muestra la misma idea sobre una tabla de casos, y el verificador la marca como ilustrativa.
El parseo es idéntico en todos; lo que de verdad distingue a los diez lenguajes es su ecosistema de dependencias. Esta es la comparación que importa en la práctica profesional.
| Lenguaje | Gestor de paquetes | Lockfile |
|---|---|---|
| Python | pip / Poetry / uv | requirements.txt (con hashes), poetry.lock, uv.lock |
| JavaScript / TypeScript | npm / pnpm / yarn | package-lock.json, pnpm-lock.yaml, yarn.lock |
| Java | Maven / Gradle | pom.xml (versiones fijas), gradle.lockfile |
| C# | NuGet | packages.lock.json |
| Go | módulos de Go | go.mod + go.sum (hashes) |
| Rust | Cargo | Cargo.lock |
| C | sistema (apt), vcpkg, Conan | conan.lock / gestión manual |
| SQL | según el motor (extensiones) | no aplica de forma estándar |
| PHP | Composer | composer.lock |
Tres observaciones con sustancia. Primera: casi todos incluyen hashes de integridad (go.sum, -i --hash en pip, el campo integrity de npm), de modo que el lockfile no solo fija la versión sino que detecta si el paquete descargado fue alterado. Segunda: Go y Rust son especialmente estrictos —el lockfile es la norma, no una opción—, mientras que en Python conviven varios formatos según la herramienta. Tercera: el mundo de C carece de un gestor universal; por eso las dependencias suelen fijarse a mano o mediante vcpkg/Conan, y la reproducibilidad cuesta más.
SemVer nació en el ecosistema Node pero hoy es lingua franca: npm, Cargo, pip, Composer y Go lo interpretan con matices propios (Go, por ejemplo, incorpora el mayor en la ruta de importación a partir de v2). El lockfile viaja con él en cada familia —package-lock.json, Cargo.lock, poetry.lock, composer.lock, go.sum— porque el problema que resuelve, reproducir un árbol de dependencias exacto, es universal.
Los mismos casos para todas las implementaciones: casos.json. Verifica la equivalencia:
python scripts/verificar_equivalencia.py 143
Detalle en reto.md.
.gitignore es el error más frecuente: cada máquina re-resuelve los rangos y termina con árboles distintos. El lockfile debe entrar en el repositorio como cualquier otro archivo fuente.latest. Suena cómodo, pero significa aceptar sin revisión cualquier cambio, incluidas rupturas mayores. Declara rangos acotados (^, ~) y deja que el lock fije lo exacto.^ con ~. ^1.4.2 acepta hasta 1.999.x; ~1.4.2 normalmente solo parches. Elegir mal amplía o estrecha silenciosamente tu superficie de riesgo.npm install, cargo update) para que los hashes sigan siendo coherentes.npm ci, pip install --require-hashes, cargo build --locked) para builds deterministas.Libros de la parte:
Libros de los lenguajes del núcleo:
⏮️ Clase 142 · 📂 Parte · 📚 Índice · 🌐 Atlas · Clase 144 ⏭️