Clase 149 — Diseño y arquitectura comparada

Parte 9 — Ingeniería de software políglota · ⏱️ Duración estimada: 90 min · Nivel: IntermedioClase construida — 10 implementaciones del núcleo verificadas contra casos.json.


🎯 Objetivo

El diseño es el conjunto de decisiones que reparten un sistema en piezas y definen cómo se hablan entre sí; la arquitectura es ese diseño a la escala más alta, la que fija la forma general antes de escribir una sola línea de negocio. McConnell dedica en Code Complete varios capítulos a lo que llama el desafío central del diseño de software: gestionar la complejidad dividiendo el sistema en partes que quepan, cada una, en una sola cabeza. Dos conceptos gobiernan esa división. La cohesión mide cuánto pertenecen juntas las cosas dentro de un módulo —alta cohesión es bueno, una capa que hace una cosa—; el acoplamiento mide cuánto depende un módulo de otro —bajo acoplamiento es bueno, porque permite cambiar una pieza sin arrastrar a las demás—. Hunt y Thomas, en The Pragmatic Programmer, empaquetan la misma meta bajo la palabra ortogonalidad: componentes independientes, donde tocar uno no repercute en los otros.

El programa de casos.json toma la medida más elemental imaginable de una arquitectura: contar sus capas. Recibe los nombres de las capas (web api datos) y responde capas=3. Es deliberadamente humilde —contar no es diseñar—, pero nombrar y enumerar los componentes es siempre el primer acto de razonar sobre una estructura: no puedes hablar del acoplamiento entre capas que aún no has identificado. La arquitectura en capas clásica (presentación → lógica → datos) es el ejemplo canónico, y contar sus estratos es el punto de partida.

Lo verdaderamente interesante para un curso políglota aparece en la comparación: cada lenguaje empuja hacia una forma distinta de organizar el código. Java te da paquetes y una jerarquía de directorios; Go y Rust, módulos con reglas de visibilidad propias; C# organiza con namespaces y ensamblados. La arquitectura no es neutral respecto al lenguaje: la gramática del lenguaje moldea la estructura que resulta natural.

📚 Resultados de aprendizaje

Al finalizar, podrás:

  1. Contar e identificar las capas o componentes de una arquitectura descrita por sus nombres.
  2. Distinguir cohesión de acoplamiento y explicar por qué se busca alta cohesión y bajo acoplamiento.
  3. Relacionar la separación de responsabilidades con la capacidad de cambiar el sistema sin efectos colaterales.
  4. Comparar cómo distintos lenguajes (paquetes Java, módulos Go/Rust, namespaces C#) empujan hacia distintas estructuras.

🗺️ Temas

# Tema Por qué importa
1 Arquitectura y capas Estructura de alto nivel del sistema
2 Cohesión y acoplamiento Decide qué tan fácil es cambiar
3 Separación de responsabilidades Cada parte hace una sola cosa
4 Módulos por lenguaje La gramática moldea la estructura

📖 Definiciones y características

🧩 Situación

Heredas un servicio que "nadie se atreve a tocar". Al leerlo descubres que la lógica de negocio hace consultas SQL directas y también arma HTML: las tres responsabilidades —presentación, negocio y datos— viven mezcladas en las mismas funciones. Cambiar el formato de una fecha en pantalla te obliga a entender la consulta a la base de datos, porque están enredadas: alto acoplamiento, baja cohesión. La cura es arquitectónica: separar en capas web, api y datos, cada una con su responsabilidad, con dependencias que apunten en una sola dirección. El primer paso, antes de mover una línea, es exactamente lo que hace el programa de esta clase: nombrar las capas y contarlas, para tener un mapa sobre el que razonar. capas=3 es el diagnóstico inicial de una arquitectura que por fin se puede discutir.

🧮 Modelo

Especificación y verificación en casos.json:

stdin esperado
web api datos capas=3
cli capas=1
web api datos cache capas=4

📐 Algoritmo (pseudocódigo neutral)

Contar los componentes es la medida más básica de una estructura: sin enumerarlos no hay nada sobre lo que razonar.

LEER capas ; ESCRIBIR cantidad

🌐 Implementaciones idiomáticas — el código a la vista

Mismo algoritmo, forma idiomática en cada lenguaje. Todas producen la salida de casos.json. Cada bloque es el archivo real de implementaciones/: el enlace de cada lenguaje abre su fuente, y el comando de al lado lo ejecuta.

Python · python/main.py · python main.py

Python resuelve el conteo con una elegancia casi telegráfica. sys.stdin.read().split() sin argumentos parte por cualquier bloque de espacios en blanco y descarta los vacíos, así que "web api datos" se convierte en una lista de tres cadenas; len(...) la mide. El detalle idiomático que Ramalho subraya en Fluent Python es que str.split() sin separador ya normaliza espacios múltiples y saltos de línea, por lo que no hace falta limpieza previa: la abstracción correcta hace desaparecer el caso borde.

import sys

capas = sys.stdin.read().split()
print(f"capas={len(capas)}")

🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R

Para web api datos la lista tiene tres elementos y sale capas=3; para cli, uno, capas=1; para web api datos cache, cuatro. Fíjate en que el "modelo" del sistema —sus capas— es aquí, literalmente, una lista, y contar componentes es medir esa lista.

Go · go/main.go · go run main.go

Go contrasta en la forma de leer, pero coincide en el espíritu con strings.Fields, que es el equivalente exacto del split() sin argumentos de Python: trocea por espacios en blanco y omite los vacíos. Donovan y Kernighan, en The Go Programming Language, presentan Fields como la herramienta idiomática para tokenizar texto separado por espacios sin sorpresas. La verbosidad de importar bufio, os y strings es el precio que Go paga por su explicitud: nada de magia, cada dependencia a la vista.

package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)

func main() {
    line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
    capas := strings.Fields(line)
    fmt.Printf("capas=%d\n", len(capas))
}

🧬 El mismo programa en la familia Sistemas: Zig · Nim · D

Que Go se apoye en strings.Fields no es casual: es un lenguaje que organiza el propio código en paquetes con visibilidad por mayúscula/minúscula, y su biblioteca estándar refleja esa mentalidad modular y sin adornos.

SQL · sql/main.sql · sqlite3 :memory: < main.sql

SQL vuelve a mostrar el otro paradigma. No hay lista ni tokens: hay una tabla donde cada capa es una fila, y contar componentes es la agregación count(*). Es una traducción fiel de la idea arquitectónica —cada componente, una entidad— al modelo relacional que Date defiende en SQL and Relational Theory: razonas sobre conjuntos de filas, no sobre una cadena que hay que partir.

-- SQL: cuenta las filas (capas).
WITH capas(nombre) AS (VALUES ('web'), ('api'), ('datos'))
SELECT printf('capas=%d', count(*)) AS resultado FROM capas;

🧬 El mismo programa en la familia Lógica y declarativa: Prolog · Datalog

JavaScript · javascript/main.mjs · node main.mjs

import { readFileSync } from "node:fs";

const capas = readFileSync(0, "utf8").trim().split(/\s+/);
console.log(`capas=${capas.length}`);

🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript

TypeScript · typescript/main.ts · pnpm exec tsx main.ts

import { readFileSync } from "node:fs";

const capas: string[] = readFileSync(0, "utf8").trim().split(/\s+/);
console.log(`capas=${capas.length}`);

🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript

Java · java/Main.java · java Main.java

import 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[] capas = br.readLine().trim().split("\\s+");
        System.out.println("capas=" + capas.length);
    }
}

🧬 El mismo programa en la familia JVM: Kotlin · Scala · Groovy · Clojure

C# · csharp/Program.cs · dotnet run

using System;

string[] capas = Console.In.ReadToEnd()
    .Split(new[] { ' ', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
Console.WriteLine($"capas={capas.Length}");

🧬 El mismo programa en la familia .NET: F# · VB.NET

Rust · rust/main.rs · rustc main.rs -o main && ./main

use std::io::Read;

fn main() {
    let mut s = String::new();
    std::io::stdin().read_to_string(&mut s).unwrap();
    let n = s.split_whitespace().count();
    println!("capas={n}");
}

🧬 El mismo programa en la familia Sistemas: Zig · Nim · D

C · c/main.c · cc main.c -o main && ./main

#include <stdio.h>

int main(void) {
    char tok[256];
    int c = 0;
    while (scanf("%255s", tok) == 1) c++;
    printf("capas=%d\n", c);
    return 0;
}

🧬 El mismo programa en la familia C / llaves: C++ · Objective-C

PHP · php/main.php · php main.php

<?php
$capas = preg_split('/\s+/', trim(fgets(STDIN)));
echo "capas=" . count($capas) . "\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.

🔬 Comparación

Aquí la diferencia entre lenguajes va más allá de la sintaxis del conteo: cada uno ofrece un mecanismo distinto para organizar el código en unidades, y ese mecanismo condiciona la arquitectura que emerge de forma natural.

Lenguaje Unidad de organización Control de visibilidad
Java paquetes (package a.b.c) + directorios public/protected/private/paquete
C# namespaces + ensamblados public/internal/private
Go paquetes (por directorio) mayúscula = exportado
Rust módulos (mod) + crates pub explícito, privado por defecto
Python módulos y paquetes (__init__.py) convención _privado
JavaScript/TS módulos ES (import/export) export explícito
C archivos + cabeceras .h static = interno a la unidad
SQL esquemas permisos por objeto
Clase de diferencia Observación entre lenguajes
Sintáctica split/Fields/count frente a bucle con scanf.
Semántica Cada capa aísla una responsabilidad; contar es el primer diagnóstico.
Paradigmática SQL cuenta filas (una capa = una fila) en vez de tokenizar.

🧬 El concepto en la familia

La estructura en componentes con responsabilidades reaparece en cada estilo arquitectónico: la arquitectura en capas las apila y dirige las dependencias hacia abajo; la hexagonal (puertos y adaptadores) pone el dominio en el centro y el mundo exterior en los bordes; los microservicios separan las capas en procesos independientes desplegables por separado. Cambian las fronteras y su coste, pero la pregunta de fondo es siempre la misma que la de esta clase: ¿cuántas piezas hay, qué responsabilidad tiene cada una y cómo dependen entre sí?

✅ Prueba común

Los mismos casos para todas las implementaciones: casos.json. Verifica la equivalencia:

python scripts/verificar_equivalencia.py 149

🧪 Reto de transferencia

Detalle en reto.md.

⚠️ Errores comunes

❓ Preguntas frecuentes

🔗 Referencias

Libros de la parte:

Libros de los lenguajes del núcleo:


⏮️ Clase 148 · 📂 Parte · 📚 Índice · 🌐 Atlas · Clase 150 ⏭️