Dependencias cíclicas e interfaces en Golang

Soy un desarrollador de python desde hace mucho tiempo. Estaba probando Go, convirtiendo una aplicación existente de python en Go. Es modular y funciona muy bien para mí.

Al crear la misma estructura en Go, parece que caigo en errores de importación cíclicos, mucho más de lo que quiero. Nunca tuve problemas de importación en Python. Ni siquiera tuve que usar alias de importación. Entonces pude haber tenido algunas importaciones cíclicas que no eran evidentes en Python. De hecho, me parece extraño.

De todos modos, estoy perdido, tratando de arreglar esto en Go. He leído que las interfaces se pueden usar para evitar dependencias cíclicas. Pero no entiendo cómo. No encontré ningún ejemplo sobre esto tampoco. ¿Puede alguien ayudarme en esto?

La estructura actual de la aplicación python es la siguiente:

/main.py /settings/routes.py contains main routes depends on app1/routes.py, app2/routes.py etc /settings/database.py function like connect() which opens db session /settings/constants.py general constants /apps/app1/views.py url handler functions /apps/app1/models.py app specific database functions depends on settings/database.py /apps/app1/routes.py app specific routes /apps/app2/views.py url handler functions /apps/app2/models.py app specific database functions depends on settings/database.py /apps/app2/routes.py app specific routes 

settings/database.py tiene funciones genéricas como connect() que abre una sesión de db. Entonces, una aplicación en el paquete de aplicaciones llama a database.connect() y se abre una sesión de db.

Lo mismo ocurre con settings/routes.py que tiene funciones que permiten a las aplicaciones agregar sus rutas secundarias al objeto de ruta principal.

El paquete de configuraciones trata más sobre funciones que datos / constantes. Este contiene el código que usan las aplicaciones en el paquete de aplicaciones, que de otro modo tendría que duplicarse en todas las aplicaciones. Entonces, si necesito cambiar la clase de enrutador, por ejemplo, solo tengo que cambiar la settings/router.py y las aplicaciones continuarán funcionando sin modificaciones.

Hay dos piezas de alto nivel para esto: averiguar qué código va en qué paquete y ajustar sus API para reducir la necesidad de que los paquetes tomen tantas dependencias.

Al diseñar API que evitan la necesidad de algunas importaciones:

  • Escribir funciones de configuración para conectar paquetes entre sí en tiempo de ejecución en lugar de tiempo de comstackción . En lugar de las routes importan todos los paquetes que definen las rutas, puede exportar routes.Register . routes.Register , a qué main (o código en cada aplicación) puede llamar. En general, la información de configuración probablemente fluya desde el paquete main o dedicado; no querrás que se distribuya en tu aplicación.

  • Pase alrededor de los tipos básicos y valores de interface . Si depende de un paquete solo para un nombre de tipo, quizás pueda evitarlo. Tal vez algún código que maneja una página []Page puede usar una []string de nombres de archivos []int o una []int de ID o alguna otra interfaz más general ( sql.Rows ).

  • Considere la posibilidad de tener paquetes de ‘esquema’ con solo tipos de datos puros e interfaces, de modo que el User esté separado del código que pueda cargar a los usuarios de la base de datos. No tiene que depender mucho (tal vez en cualquier cosa), por lo que puede incluirlo desde cualquier lugar. Ben Johnson dio una charla relámpago en GopherCon 2016 sugiriendo eso y organizando paquetes por dependencias.

En la organización de código en paquetes:

  • Como regla general, divida un paquete cuando cada pieza pueda ser útil por sí misma . Si dos funciones están íntimamente relacionadas, no es necesario dividirlas en paquetes; puede organizar con múltiples archivos o tipos en su lugar. Los paquetes grandes pueden estar bien; La net/http Go es una, por ejemplo.

  • Rompe los paquetes de bolsas de utils ( tools , tools ) por tema o dependencia. De lo contrario, puede terminar importando un enorme paquete de utils (y tomando todas sus dependencias) para una o dos piezas de funcionalidad (que no tendrían tantas dependencias si se separaran).

  • Considere empujar el código reutilizable hacia abajo en paquetes de nivel inferior desenredados de su caso de uso particular. Si tiene una package page contiene tanto lógica para su sistema de administración de contenido como código de manipulación HTML de uso múltiple, considere mover el material HTML “hacia abajo” a un package html para que pueda usarlo sin importar material de administración de contenido no relacionado.


Aquí, reorganizaría las cosas para que el enrutador no tenga que incluir las rutas: en su lugar, cada paquete de aplicación llama a un método router.Register() . Esto es lo que hace el paquete mux la herramienta web Gorilla . Sus paquetes de routes , database y constants suenan como piezas de bajo nivel que el código de la aplicación debe importar y no importarlo.

Generalmente, intenta construir tu aplicación en capas. El código de la aplicación específica de cada caso de capa superior debería importar herramientas de capa inferior y más fundamentales, y nunca al revés. Aquí hay algunos pensamientos más:

  • Los paquetes son para separar partes de funcionalidad utilizables independientemente ; no es necesario dividir uno cada vez que un archivo fuente se agrande. A diferencia de, por ejemplo, Python o Java, en Go uno puede dividir, combinar y reorganizar archivos completamente independientes de la estructura del paquete, de modo que puede descomponer archivos enormes sin romper paquetes.

    La net/http la biblioteca estándar es de aproximadamente 7k líneas (contando comentarios / espacios en blanco pero no pruebas). Internamente, se divide en muchos archivos y tipos más pequeños. Pero es un paquete, creo, porque no había ninguna razón por la que los usuarios quisieran, digamos, manejar las cookies por sí solo. Por otro lado, net y net/url están separados porque tienen usos fuera de HTTP.

    Es genial si puede insertar utilidades “hacia abajo” en bibliotecas que son independientes y se sienten como sus propios productos pulidos, o aplicar una capa limpia a su aplicación (p. Ej., La interfaz de usuario se encuentra encima de algunas bibliotecas principales y modelos de datos). Del mismo modo, la separación “horizontal” puede ayudarlo a mantener la aplicación en su cabeza (por ejemplo, la capa de IU se divide en administración de cuenta de usuario, núcleo de la aplicación y herramientas administrativas, o algo más detallado que eso). Pero, el punto central es que eres libre de dividir o no, ya que funciona para ti .

  • Configure API para configurar el comportamiento en tiempo de ejecución para que no tenga que importarlo en tiempo de comstackción. Por lo tanto, por ejemplo, su enrutador de URL puede exponer un método de Register lugar de importar appA , appB , etc. y leer una var Routes de cada uno. Puede hacer un paquete myapp/routes que importe el router y todas sus vistas y llamadas al router.Register . router.Register . La idea fundamental es que el enrutador es un código de uso múltiple que no necesita importar las vistas de su aplicación.

    Algunas formas de armar las API de configuración:

    • Pasar el comportamiento de la aplicación a través de las interface o func : http se puede pasar implementaciones personalizadas de Handler (por supuesto) pero también de CookieJar o File . text/template y html/template pueden aceptar que las funciones sean accesibles desde las plantillas (en un FuncMap ).

    • Exporte las funciones de acceso directo de su paquete si corresponde: en http , las personas que llaman pueden configurar y configurar por separado algunos objetos http.Server o llamar a http.ListenAndServe(...) que usa un Server global. Eso le da un diseño agradable, todo está en un objeto y las personas que llaman pueden crear múltiples Server en un proceso y tal, pero también ofrece una forma perezosa de configurar en el caso simple de un solo servidor.

    • Si es necesario, simplemente con cinta adhesiva: no tiene que limitarse a sistemas de configuración súper elegantes si no puede encajar uno en su aplicación: tal vez para algunas cosas un package "myapp/conf" con un sistema global var Conf map[string]interface{} es útil. Pero tenga en cuenta las desventajas de la configuración global. Si desea escribir bibliotecas reutilizables, no pueden importar myapp/conf ; tienen que aceptar toda la información que necesitan en los constructores, etc. Los globales también corren el riesgo de sufrir un cableado en la suposición de que algo siempre tendrá un valor único en toda la aplicación cuando finalmente no lo hará; tal vez hoy tengas una única configuración de base de datos o una configuración de servidor HTTP o tal, pero algún día no lo harás.

Algunas formas más específicas de mover el código o cambiar las definiciones para reducir los problemas de dependencia:

  • Separe las tareas fundamentales de las que dependen de la aplicación. Una aplicación en la que trabajo en otro idioma tiene un módulo “utils” que mezcla tareas generales (por ejemplo, formateo de fechas o trabajo con HTML) con elementos específicos de la aplicación (que depende del esquema del usuario, etc.). Pero el paquete de usuarios importa los utils, creando un ciclo. Si estuviera migrando a Go, movería las utilidades dependientes del usuario “hacia arriba” del módulo utils, tal vez para vivir con el código de usuario o incluso encima.

  • Considere dividir paquetes de bolsas de sorpresas. Aumento ligero en el último punto: si dos piezas de funcionalidad son independientes (es decir, las cosas aún funcionan si mueves algún código a otro paquete) y no están relacionadas desde la perspectiva del usuario, son candidatas para separarlas en dos paquetes. A veces, la agrupación es inofensiva, pero otras veces genera dependencias adicionales, o un nombre de paquete menos genérico podría generar un código más claro. Por lo tanto, mis utils anteriores podrían dividirse por tema o dependencia (por ejemplo, strutil , dbutil , etc.). Si goimports con muchos paquetes de esta manera, tenemos goimports para ayudar a administrarlos.

  • Reemplace los tipos de objetos que requieren importación en API con tipos básicos e interface . Supongamos que dos entidades en su aplicación tienen una relación de muchos a muchos como User y Group . Si viven en paquetes diferentes (un gran ‘si’), no puede hacer que u.Groups() devuelva un grupo []group.Group y g.Users() devuelvan []user.User porque eso requiere que los paquetes importar uno al otro.

    Sin embargo, puede cambiar uno o ambos de los que devuelvan, digamos, un número de ID o un sql.Rows o alguna otra interface que pueda acceder sin import un tipo de objeto específico. Dependiendo de su caso de uso, los tipos como User y Group pueden estar tan íntimamente relacionados que es mejor colocarlos en un solo paquete, pero si decide que deben ser distintos, esta es una forma.

Gracias por la pregunta detallada y el seguimiento.

Básicamente, su código está altamente acoplado, y Golang lo obliga a mantener los paquetes poco acoplados, pero dentro de un paquete la alta cohesión está bien.

Golang es muy superior en comparación con Python con respecto a la gestión de paquetes. En python, incluso puede importar paquetes dinámicamente.

Para grandes proyectos, Golang asegurará que sus paquetes sean más fáciles de mantener.