Al programar en TypeScript o Angular nos encanta usarlos tipos y a veces no prestamos atención a las "Circular dependencies" (dependencias circulares). Creamos tipos e intentamos usarlos en todas partes para coincidir con la estructura existente, y si no tenemos cuidado, es muy fácil crear una referencia circular y un bucle de dependencia que puede ser problemático en nuestro código.

Hoy, quiero mostrar qué tan fácil es crear dependencias circulares y cómo solucionarlas.

CPU
1 vCPU
MEMORIA
1 GB
ALMACENAMIENTO
10 GB
TRANSFERENCIA
1 TB
PRECIO
$ 4 mes
Para obtener el servidor GRATIS debes de escribir el cupon "LEIFER"

Escenario
Estamos construyendo una aplicación que muestra botones basados en un tema. Los botones deben tener un tema y una etiqueta, y el usuario puede establecer una configuración o usar una predeterminada.

Estamos usando TypeScript. Para asegurarte de tener TypeScript instalado, si no, ejecuta el comando:

npm i -g typescript

Después de eso, crea un nuevo directorio desde la terminal y genera una configuración predeterminada de TypeScript con tsc --init:

mkdir circular-references
cd circular-references
tsc --init

Crear los tipos
Primero, creamos theme.ts con las propiedades relacionadas con colores y fontFormat.

export type Theme = {
  color: string;
  fontFormat: string;
};

Luego, porque queremos mantener nuestro código dividido, creamos el archivo button.ts para los botones. El tipo button tiene dos propiedades: label como una cadena y theme del tipo Theme.

Recuerda importar Theme
import { Theme } from "./theme";

export type ButtonConfig = {
    label: string;
    theme: Theme;
};

Este código parece estar bien sin ninguna dependencia circular, perfecto, pero ahora es el momento de continuar con nuestro proyecto.

Usando el tema
Queremos proporcionar dos tipos de temas: DarkTheme y LightTheme, y una configuración predeterminada para los usuarios, definiendo una lista de botones basada en el tema. Abre de nuevo theme.ts y creamos los temas.

export const DarkTheme: Theme = {
    color: 'black',
    fontFormat: 'italic'
};

export const LightTheme: Theme = {
    color: 'white',
    fontFormat: 'bold'
};

A continuación, para la defaultConfig, este define una lista de botones basada en el tema. Vamos a importar ButtonConfig y asignar el tema.

import { ButtonConfig } from "./button";

export const defaultThemeConfig: ThemeConfig = {
    buttons: [
        {
            theme: DarkTheme,
            label: 'Accept'
        },
        {
            theme: DarkTheme,
            label: 'Cancel'
        }
    ],
    type: 'dark'
};

Todo parece estar funcionando según lo esperado.

  • Dividimos los tipos para botones y temas en archivos separados.
  • Creamos temas DarkTheme y DarkTheme.
  • Configuramos una configuración predeterminada.

Parece que nuestro código funciona correctamente. Vamos a usarlo.

Construir aplicación de ejemplo
Creamos una aplicación de ejemplo con la función de mostrar botones basados en la configuración. Si no hay configuración, entonces usar defaultThemeConfig.

Crea el archivo main.ts e importa defaultThemeConfig. Crea la aplicación con una función showButtons, dentro verifica si la configuración no existe usa defaultThemeConfig.

El código se ve así:

import { defaultThemeConfig } from "./theme-config"; 

const app = {
    config: undefined,
    showButtons() {
        const config = this.config ?? defaultThemeConfig;
        console.log(config);
    }
};
app.showButtons();

En la terminal, compila main.ts usando tsc y ejecuta con node.

$circular-references>tsc main.ts
$circular-references>node main.js

¡Sí! Nuestra aplicación funciona como se esperaba, pero espera un minuto. ¿Viste que theme.ts requiere button.ts y button.ts usa theme.ts?

Referencia circular 😖
Creamos una referencia circular. ¿Por qué? Porque los botones requieren el tema y viceversa. ¿Por qué sucedió esto?

Porque creamos el Theme y ThemeConfig en el mismo archivo, mientras también teníamos una dependencia en ButtonConfig.

La clave de la dependencia circular fue:

  • theme.ts definido Theme y quería usar ButtonConfig (que requería Theme).
  • button.ts  que depende tambien de theme.ts.

En mi caso, es fácil de ver, pero si tienes un proyecto ya grande, la mejor manera de ver tu dependencia circular es con el paquete madge, que informa todos los archivos con dependencias circulares.

Madge es una herramienta asombrosa para generar un gráfico visual de las dependencias del proyecto, encontrar dependencias circulares y dar otra información útil [leer más]

En nuestro caso, ejecuta el comando npx madge -c --extensions ts ./.

Ok, tengo el problema, ¿cómo lo soluciono? 🤔

Solucionando la dependencia circular


Para solucionar el problema de referencia circular entre theme.ts y button.ts, debemos crear un nuevo archivo para romper las relaciones y asegurar que las dependencias entre estos archivos sean unidireccionales, extrayendo las dependencias comunes en un archivo separado.

En nuestro caso, podemos mover todo lo relacionado con la configuración del tema ThemeConfig y la configuración predeterminada en un nuevo archivo theme-config.ts.

Crear un archivo específico para ThemeConfig nos ayuda a mantener las configuraciones relacionadas con el tema separadas de las definiciones de tema y botón.

Vamos a refactorizar

Theme.ts

El theme.ts solo necesita exportar tipos que contienen las definiciones para Theme y las instancias del tema como DarkTheme y LightTheme.

export type Theme = {
  color: string;
  fontFormat: string;
};

export const DarkTheme: Theme = {
  color: 'black',
  fontFormat: 'italic',
};

export const LightTheme: Theme = {
  color: 'white',
  fontFormat: 'bold',
};

Button.ts
Ahora, modifica el archivo button.ts para tipo, que se basa en Theme de theme.ts.

import { Theme } from "./theme";

export type ButtonConfig = {
    label: string;
    theme: Theme;
};

Theme-config.ts
Crea theme-config.ts que contiene ThemeConfig y la configuración predeterminada para el tema, utilizando ButtonConfig de button.ts e indirectamente, Theme de theme.ts.

import { ButtonConfig } from "./button";
import { DarkTheme } from "./theme";

export type ThemeConfig = {
    type: 'dark' | 'light';
    buttons: Array<ButtonConfig>;
};

export const defaultThemeConfig: ThemeConfig = {
    buttons: [
        {
            theme: DarkTheme,
            label: 'Accept'
        },
        {
            theme: DarkTheme,
            label: 'Cancel'
        }
    ],
    type: 'dark'
};

Ejecuta de nuevo madge y ¡voilà! 🎉

¿Qué hicimos?


Sí, solucionamos la dependencia circular haciendo un pequeño cambio estructural:

  • theme.ts es independiente y define el tipo base Theme y los objetos DarkTheme y LightTheme.
  • button.ts depende de theme.ts para el tipo Theme.
  • theme-config.ts depende de ambos, button.ts para el tipo ButtonConfig y theme.ts para los objetos del tema, reuniéndolos en un objeto de configuración.

Eliminamos la dependencia circular organizando el código en una dependencia más lineal: theme.tsbutton.tstheme-config.ts. Cada archivo tiene una responsabilidad clara, y la dirección de dependencia es desde la definición del tema y la configuración del botón.

Espero que esto te ayude a pensar y solucionar tus dependencias circulares, o incluso evitarlas por completo en el futuro 🚀