Read in English · Lire en français · Auf Deutsch lesen
Sección 0.3 de 0.8 · Principiante · 16 min de lectura
Funciones, módulos, objetos, interfaces, bibliotecas y frameworks
Videolección
5 min · narrado en inglés, con subtítulosSolo escuchar (audio)
Leer la transcripción
Piensa en un restaurante grande. En lugar de un solo cocinero que lo hace todo, hay estaciones: parrilla, repostería y ensaladas, cada una con su propio personal, sus herramientas y sus recetas. Los pedidos pasan de una estación a otra mediante comandas, así que la parrilla no necesita saber cómo trabaja el repostero. El código se organiza exactamente igual, y esta lección te muestra cómo.
El bloque de construcción más pequeño es la función: un fragmento de código con nombre que hace una sola tarea. Piensa en una cafetera. Pones granos, agua y eliges una bebida, y sale un café. Las funciones te permiten escribir una regla una vez y reutilizarla. Una tienda podría escribir su cálculo del IVA en una función y llamarla desde el proceso de pago, la factura y la pantalla de reembolsos. Así, cuando cambia el tipo de IVA, cambia una función, no veinte copias dispersas que pronto acabarían siendo distintas.
Después, las funciones relacionadas se agrupan en módulos, y los módulos relacionados, en un paquete. Aquí, un paquete llamado shop contiene dos módulos: pricing y orders. Cada módulo tiene una pequeña parte pública, pensada para que la usen otros módulos, y detalles privados. En el módulo pricing, la función del IVA es pública. El auxiliar que está debajo es privado, y en Python se marca con un guion bajo inicial. El módulo orders solo llama a la función pública de pricing, nunca al auxiliar privado. Piensa en un departamento con un mostrador de atención público y procesos internos que nadie de fuera necesita ver. El valor de esta división es que la parte privada se puede reescribir libremente sin romper nada a nadie, siempre que la parte pública cumpla sus promesas.
Muchos lenguajes permiten reunir los datos con las funciones que trabajan sobre ellos. Una clase es la plantilla, y un objeto es una cosa concreta hecha a partir de ella. Una clase de cuenta bancaria dice que toda cuenta tiene un titular y un saldo; la cuenta de cada cliente es un objeto. La idea clave es el encapsulamiento. Si cualquier código pudiera asignar el saldo directamente, nada impediría que quedara en negativo. En cambio, el método withdraw es la única puerta, y comprueba las reglas cada vez, como un cajero de banco delante de la cámara acorazada.
La idea más importante de esta lección es la interfaz. Es un contrato: las operaciones que una parte ofrece a las demás, qué reciben, qué devuelven y qué puede salir mal. Un enchufe es una interfaz: cualquier aparato con la clavija adecuada funciona. Veamos por qué importa. Supón que el proceso de pago de una tienda llama directamente a las funciones propias de un proveedor de pagos, por todas partes. Si la tienda cambia de proveedor el año que viene, hay que reescribir el proceso de pago. En cambio, la tienda define su propia interfaz pequeña de pasarela de pagos, con una sola operación: cobrar este importe a esta tarjeta. Escribe un adaptador por proveedor. El proceso de pago depende solo de la interfaz, así que cambiar de proveedor significa escribir un adaptador nuevo. Además, facilita las pruebas. En las pruebas, una pasarela falsa que nunca contacta con un banco real puede sustituir a la verdadera.
Casi nadie escribe todo desde cero. Una biblioteca es código que escribió otra persona y al que tu código llama cuando lo necesita, como una biblioteca para fechas o para crear archivos PDF. Tu código sigue al mando. Un framework funciona al revés. Proporciona el esqueleto de la aplicación y llama a tu código en los momentos adecuados. Esto se llama inversión de control: no nos llames, nosotros te llamaremos. Los frameworks ahorran mucho trabajo de diseño, pero suelen ser difíciles de sustituir más adelante.
Cada biblioteca o framework que usas es una dependencia, que obtiene un gestor de paquetes como pip para Python o npm para JavaScript. Aquí, una app de tienda elige tres paquetes directamente. Pero esos paquetes traen otros cinco propios, llamados dependencias transitivas. Elegiste tres paquetes, pero dependes de ocho. Cada uno es código que no escribiste tú, y se ejecuta con todos los permisos del tuyo. Así que un defecto en cualquiera de ellos, como este paquete de criptografía, es un defecto en tu app.
¿Por qué importa tanto toda esta estructura? Porque la mayor parte del costo del software se dedica a cambiarlo. Dos ideas describen una buena estructura. El acoplamiento es cuánto depende una parte de los detalles de otra. La cohesión es lo bien que encaja entre sí el contenido de una parte. A la izquierda, cuatro módulos acceden todos a los datos de los demás. Un cambio en uno puede propagarse a todos los demás. A la derecha, los mismos módulos tienen solo tres enlaces, cada uno a través de una interfaz pequeña. Ese es el objetivo: bajo acoplamiento y alta cohesión. También permite que equipos distintos trabajen y lancen de forma independiente.
La arquitectura es esta misma actividad a mayor escala: decidir las partes principales, sus interfaces y en qué dirección apuntan las dependencias. La sección completa incluye el código y una lista de comprobación para añadir dependencias. A continuación, en la Sección cero punto cuatro, el código sale de la computadora única y vemos cómo se comunican las computadoras.
Lo que necesitas saber
- Una función es un fragmento de código reutilizable y con nombre, que tiene entradas y una salida; permite escribir una regla una sola vez y cambiarla en un único lugar.
- Los módulos y los paquetes agrupan código relacionado y separan una pequeña parte pública de los detalles privados, que pueden cambiar libremente.
- Las clases reúnen los datos con los métodos que actúan sobre ellos; el encapsulamiento mantiene los datos privados para que las reglas se cumplan siempre.
- Una interfaz es un contrato entre partes; el código que depende de una interfaz y no de una implementación puede cambiar de implementación con facilidad.
- Tú llamas a una biblioteca; un framework llama a tu código. Esta inversión de control hace que los frameworks sean potentes, pero más difíciles de sustituir.
- Las dependencias, que obtienen gestores de paquetes como pip y npm, ahorran esfuerzo, pero traen código transitivo, riesgos de seguridad y trabajo de mantenimiento.
- Una buena estructura significa bajo acoplamiento y alta cohesión, lo que mantiene barato el cambio y permite que los equipos trabajen de forma independiente.
Por qué organizar el código
Un pequeño script de veinte líneas se puede leer de principio a fin en un minuto. Un sistema real, como una plataforma de reparto de comida, puede tener cientos de miles o millones de líneas escritas por decenas de personas a lo largo de muchos años. Nadie puede tener todo eso en la cabeza. La única forma de trabajar en un sistema así es dividirlo en partes que se puedan entender, probar y cambiar de una en una, con reglas claras sobre cómo pueden usarse unas a otras.
Los lenguajes de programación nos ofrecen para ello una escalera de bloques de construcción, de menor a mayor: funciones, módulos y paquetes, clases y objetos, y las interfaces entre ellos. Por encima del código que escribimos nosotros están las bibliotecas y los frameworks escritos por otros. Esta sección sube esa escalera. Esas mismas ideas, aplicadas a mayor escala, son el objeto de la arquitectura de software.
Funciones: poner nombre a una tarea y reutilizarla
Una función es un fragmento de código con nombre que hace una sola tarea. Recibe entradas (llamadas parámetros o argumentos), hace un trabajo y normalmente devuelve una salida (su valor de retorno). Piensa en una cafetera: pones granos, agua y eliges una bebida; la máquina hace su trabajo; sale un café. No necesitas saber cómo funciona la bomba para usarla.
Las funciones aportan tres beneficios. Reutilización: el cálculo del IVA se escribe una vez y se llama desde el proceso de pago, la factura y la pantalla de reembolsos. Legibilidad: total_with_vat(basket) dice qué está pasando sin mostrar cómo. Cambio en un único lugar: cuando cambia el tipo de IVA, cambia una función, no veinte copias de la misma operación repartidas por el código (copias que pronto acabarían siendo distintas).
Una buena función hace una sola cosa, tiene un nombre que dice lo que hace, recibe como parámetros las entradas que necesita en lugar de ir a buscar datos ocultos, y es lo bastante corta como para leerla de un vistazo. Una función que solo calcula su salida a partir de sus entradas, sin cambiar nada más, se llama función pura; las funciones puras son especialmente fáciles de probar, porque las mismas entradas siempre dan la misma salida.
VAT_RATE = 0.20
def total_with_vat(prices_in_pence):
"""Return the total of a basket in pence, including VAT."""
subtotal = sum(prices_in_pence)
return round(subtotal * (1 + VAT_RATE))
checkout_total = total_with_vat([1250, 399]) # 1979
invoice_total = total_with_vat([5000]) # 6000Módulos y paquetes: agrupar el código relacionado
Un módulo agrupa funciones y datos relacionados en una unidad con su propio nombre. En Python, un módulo es simplemente un archivo .py; pricing.py podría contener todo lo relacionado con precios, descuentos e impuestos. Otro archivo lo usa escribiendo import pricing. Un paquete es una carpeta de módulos relacionados, así que una tienda podría tener un paquete shop que contenga los módulos pricing, orders y customers. Otros lenguajes usan palabras distintas (Java tiene paquetes, C# tiene espacios de nombres y ensamblados, JavaScript tiene módulos), pero la idea es la misma.
Un módulo es como un departamento de una organización. El departamento de finanzas tiene un mostrador de atención público donde otros pueden hacer solicitudes ("por favor, emitan esta factura") y procesos internos que nadie de fuera necesita ver. Del mismo modo, los módulos separan una pequeña parte pública, pensada para que la usen otros módulos, de los detalles privados. Python marca los nombres privados por convención con un guion bajo inicial, como _round_pence; Java y C# imponen la privacidad con palabras clave como private. El valor de esta división es que la parte privada se puede reescribir libremente sin romper nada a nadie, siempre que la parte pública cumpla sus promesas.
Funciones, módulos y un paquete
Clases y objetos: los datos con su comportamiento
Muchos lenguajes permiten reunir los datos con las funciones que trabajan sobre ellos. Una clase es la plantilla; un objeto es una cosa concreta hecha a partir de ella. Una clase BankAccount podría decir que toda cuenta tiene un titular y un saldo, y que admite depósitos y retiros. La cuenta de cada cliente es entonces un objeto distinto creado a partir de esa clase. Las funciones que pertenecen a una clase se llaman métodos. (Consulta la entrada del glosario sobre clase y objeto).
La idea más útil aquí para la arquitectura es el encapsulamiento: un objeto mantiene sus datos privados y solo deja que otro código los cambie a través de sus métodos. Si cualquiera pudiera asignar account.balance directamente, nada impediría que código de otra parte dejara un saldo en negativo o se saltara el registro de auditoría. Con el encapsulamiento, el método withdraw es la única puerta, y puede hacer cumplir las reglas cada vez. La analogía es un cajero de banco: los clientes no pueden entrar en la cámara acorazada, pero pueden pedírselo al cajero, que comprueba las reglas antes de entregar el dinero.
La programación orientada a objetos tiene mucho más (herencia, polimorfismo, patrones de diseño), y los profesionales no se ponen de acuerdo sobre cuánto de ello es útil: algunos lenguajes, como Go, omiten deliberadamente la herencia, y muchos equipos prefieren un estilo más funcional. Sin embargo, el encapsulamiento es valioso en cualquier estilo.
class BankAccount:
def __init__(self, owner):
self.owner = owner
self._balance = 0 # private by convention
def deposit(self, pence):
if pence <= 0:
raise ValueError("Deposit must be positive")
self._balance += pence
def withdraw(self, pence):
if pence > self._balance:
raise ValueError("Insufficient funds")
self._balance -= pence
def balance(self):
return self._balance
acct = BankAccount("Ana")
acct.deposit(10000)
acct.withdraw(2500)
print(acct.balance()) # 7500Interfaces: contratos entre partes
Una interfaz es el conjunto acordado de operaciones que una parte de un sistema ofrece a las demás: sus nombres, qué entradas reciben, qué devuelven y qué puede salir mal. Es un contrato. Quien llama confía en el contrato, no en cómo se hace el trabajo. Un enchufe es una interfaz: cualquier aparato con la clavija adecuada funciona, y al aparato no le importa si la electricidad procede del viento, del gas o de la energía nuclear.
En el código, una interfaz permite cambiar una implementación por otra. Supón que una tienda cobra hoy los pagos con tarjeta a través de un proveedor de pagos, pero puede cambiar de proveedor el año que viene. Si el código del proceso de pago llama directamente a las funciones propias del proveedor, por todas partes, cambiar de proveedor obliga a reescribir el proceso de pago. Si, en cambio, la tienda define su propia interfaz pequeña PaymentGateway con una sola operación, charge(amount, card_token), y luego escribe un adaptador por proveedor, el proceso de pago depende solo de la interfaz y cambiar de proveedor significa escribir un adaptador nuevo. El mismo truco facilita las pruebas: en ellas, una pasarela falsa que nunca contacta con un banco real puede sustituir a la verdadera.
Las interfaces son la idea más importante de esta sección para los arquitectos. Cada límite de un sistema, desde la firma de una función hasta una API web que usan miles de otras empresas, es una interfaz. Las buenas son pequeñas, estables y ocultan las decisiones que probablemente cambiarán; ese principio, llamado ocultación de información, lo formuló David Parnas en 1972 y se trata en el Capítulo 3.
from typing import Protocol
class PaymentGateway(Protocol):
def charge(self, amount_pence: int, card_token: str) -> bool: ...
class ProviderAGateway:
def charge(self, amount_pence, card_token):
... # call Provider A's web API
return True
class FakeGateway: # used in tests
def charge(self, amount_pence, card_token):
return amount_pence < 100_000 # pretend large payments fail
def checkout(basket_total, card_token, gateway: PaymentGateway):
return "paid" if gateway.charge(basket_total, card_token) else "declined"Bibliotecas y frameworks: código escrito por otros
Casi nadie escribe todo desde cero. Una biblioteca es código reutilizable que ha escrito otra persona y al que tu código llama cuando lo necesita: una biblioteca para manejar fechas y zonas horarias, para generar PDF, para comunicarse con una base de datos. Tú sigues al mando; la biblioteca es una herramienta que tomas y dejas.
Un framework es diferente. Proporciona el esqueleto general de una aplicación y decide cuándo ocurren las cosas; tú rellenas los huecos con tu propio código, y el framework llama a ese código en los momentos adecuados. Un framework web como Django (Python), Spring (Java), Ruby on Rails o Express (JavaScript) recibe cada solicitud web entrante, determina cuál de tus funciones debe atenderla, llama a esa función y convierte su resultado en una página web o en datos. La forma corta de recordar la diferencia: tú llamas a una biblioteca; un framework llama a tu código.
Esa inversión se llama inversión de control. Martin Fowler atribuye a Ralph Johnson y Brian Foote el uso del término en 1988, y la idea suele resumirse como el principio de Hollywood: No nos llames, nosotros te llamaremos
. Una franquicia de restaurantes es una buena analogía: la franquicia aporta el local, el sistema de pedidos, el horario de apertura y las reglas; el franquiciado aporta la cocina en los huecos que la franquicia le deja.
Quién llama a quién
Biblioteca o framework
| Biblioteca | Framework | |
|---|---|---|
| Quién controla el flujo | Tu código | El framework (inversión de control) |
| Ejemplos | Una biblioteca de fechas, un generador de PDF, un parser de JSON | Django, Spring, Ruby on Rails, Angular |
| Facilidad para sustituirlo más adelante | Normalmente moderada: cambiar los lugares que la llaman | Normalmente difícil: la estructura del framework da forma a toda la aplicación |
| Principal beneficio | Te ahorra escribir tú mismo una capacidad | Te ahorra diseñar toda la estructura; convenciones coherentes en todo el equipo |
| Principal riesgo | Una dependencia más que mantener | Dependencia (lock-in) de su forma de trabajar y de su ciclo de actualizaciones |
Dependencias y gestores de paquetes
Cada biblioteca o framework que necesita tu código es una dependencia. Los proyectos modernos tienen muchas, así que usan un gestor de paquetes para obtenerlas: pip para Python (desde el Python Package Index, PyPI) y npm para JavaScript (desde el registro de npm) son los más conocidos, con equivalentes como Maven para Java y NuGet para .NET. Un proyecto enumera lo que necesita, a menudo con rangos de versiones, y el gestor de paquetes descarga esos paquetes y, lo que es crucial, los paquetes de los que dependen ellos. Estas dependencias indirectas, transitivas, suelen superar muchas veces en número a las directas.
Las versiones suelen numerarse como MAJOR.MINOR.PATCH según una convención llamada versionado semántico: un cambio en el número mayor indica un cambio incompatible; en el número menor, nuevas funciones que no deberían romper nada a los usuarios existentes; y en el número de parche, correcciones de errores. Los proyectos también guardan un lockfile (archivo de bloqueo), como package-lock.json en npm, que registra la versión exacta de cada paquete instalado, para que la máquina de cada desarrollador y la compilación de producción usen el mismo código.
Dependencias directas y transitivas
Las dependencias ahorran un esfuerzo enorme, pero cada una es código que no escribiste tú y que se ejecuta con todos los permisos del tuyo. Dos incidentes muy conocidos muestran los riesgos. En marzo de 2016, un desarrollador retiró sus paquetes de npm a raíz de una disputa, entre ellos left-pad, una función diminuta de unas once líneas para rellenar texto; miles de proyectos que dependían de él, directa o indirectamente, dejaron de compilarse de repente hasta que npm lo restauró. En diciembre de 2021 se reveló una grave vulnerabilidad de seguridad, apodada Log4Shell, en Log4j 2, una biblioteca de logging para Java muy utilizada; organizaciones de todo el mundo pasaron semanas localizando cada sistema que la contenía, a menudo enterrada varios niveles por debajo en sus dependencias.
Lista de comprobación: antes de añadir una dependencia
Preguntas que se hace un equipo cuidadoso antes de confiar en el paquete de otra persona.
- ¿Lo necesitamos?Unas pocas líneas de código propio pueden salir más baratas con el tiempo que un paquete al que hay que seguir y actualizar.
- ¿Se mantiene?Mira los lanzamientos recientes, los problemas de seguridad abiertos y cuántas personas lo mantienen. Un paquete abandonado se convierte en tu problema.
- ¿Qué trae consigo?Comprueba cuántas dependencias transitivas añade; cada una es más código en el que confiar.
- ¿Es compatible la licencia?Las licencias de código abierto son diferentes; algunas imponen obligaciones cuando distribuyes tu software.
- ¿Están las versiones fijadas y analizadas?Usa un lockfile y una herramienta automática que avise cuando una vulnerabilidad conocida afecte a una versión que usas.
- ¿Podríamos sustituirlo?Envuelve las dependencias importantes detrás de una interfaz propia, para que un cambio de proveedor afecte a un solo lugar.
Por qué importa la estructura: el cambio y los equipos
El código se lee y se cambia muchas más veces de las que se escribe por primera vez. La mayor parte del costo de un software a lo largo de su vida se dedica a cambiarlo: corregir errores, añadir funciones, adaptarse a nuevas leyes y a nuevos socios. La estructura decide lo caro que resulta. En un sistema bien organizado, un cambio en la forma de calcular el IVA afecta a un módulo. En uno mal organizado, afecta a cuarenta archivos de cinco equipos distintos, y algo que no tiene nada que ver se rompe.
Dos ideas, descritas por primera vez por Larry Constantine y sus colegas en el movimiento del diseño estructurado de finales de la década de 1960 y de la de 1970, recogen lo que significa "bien organizado". El acoplamiento es cuánto depende una parte de los detalles de otra: en un sistema muy acoplado, cambiar una parte obliga a cambiar otras. La cohesión es lo bien que encaja entre sí el contenido de una parte: un módulo muy cohesivo hace una tarea clara. El objetivo es bajo acoplamiento y alta cohesión: partes que hacen bien una tarea cada una y que se comunican entre sí mediante interfaces pequeñas y estables.
La estructura también importa para las personas. Si dos equipos tienen que editar el mismo módulo para la mayor parte de su trabajo, se esperarán y se estorbarán constantemente. Si cada equipo es responsable de módulos cohesivos con interfaces claras, pueden trabajar y lanzar de forma independiente. Melvin Conway observó en 1968 que los sistemas tienden a reflejar la estructura de comunicación de las organizaciones que los construyen, una observación conocida como ley de Conway que se analiza en el Capítulo 3.
El acoplamiento de un vistazo
Por qué esto importa para la arquitectura
La arquitectura es en gran parte la misma actividad que esta sección, a mayor escala y con más en juego: decidir cuáles son las partes principales de un sistema, de qué se encarga cada una, qué interfaces las conectan y en qué dirección apuntan las dependencias. Un módulo dentro de un programa y un servicio desplegado por separado al otro lado de una red son, ambos, partes con interfaces; las cuestiones de acoplamiento, cohesión y ocultación de información se aplican a los dos.
El Capítulo 3 precisa estos principios, empezando por la modularidad y el acoplamiento y la cohesión, y el Capítulo 8 vuelve a las dependencias como cuestión de seguridad en la seguridad de la cadena de suministro. A continuación, el código sale de la computadora única: la siguiente sección explica cómo se comunican las computadoras entre sí.
Comprueba lo que has entendido
¿Por qué es mejor llamar a una sola función total_with_vat desde tres lugares que escribir el cálculo tres veces?
Porque así la regla está en un único lugar: cuando cambia el tipo de IVA, un solo cambio corrige a todos los que la llaman, y las copias no pueden acabar siendo distintas.
¿Qué impide el encapsulamiento que haga otro código con un objeto de cuenta bancaria?
Impide que otro código cambie el saldo directamente, de modo que todo cambio tiene que pasar por métodos como withdraw, que hacen cumplir las reglas.
Un framework web llama a tu función cada vez que llega una solicitud para /orders. ¿Cómo se llama esta forma de organizarlo?
Inversión de control: es el framework, no tu código, quien controla el flujo y llama a tu código cuando hace falta.
¿Para qué sirve un lockfile, como package-lock.json?
Registra la versión exacta de cada paquete instalado, incluidos los indirectos, para que la máquina de cada desarrollador y cada compilación de producción usen exactamente el mismo código.
Fuentes y lecturas recomendadas
¿Tienes una pregunta sobre esta sección? ArchitectBot, el asistente de estudio con IA del curso, responde primero a partir del curso.
Pregunta: “¿Cómo decido si construir una funcionalidad sobre un framework o mantener mi lógica de negocio independiente de él?”