cast - Dependencias cíclicas e interfaces en Golang
golang cast interface to struct (2)
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.
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.
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 compilación . En lugar de las
routes
importan todos los paquetes que definen las rutas, puede exportarroutes.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 paquetemain
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 deutils
(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 unpackage 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
ynet/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 compilación. Por lo tanto, por ejemplo, su enrutador de URL puede exponer un método de
Register
lugar de importarappA
,appB
, etc. y leer unavar Routes
de cada uno. Puede hacer un paquetemyapp/routes
que importe elrouter
y todas sus vistas y llamadas alrouter.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
ofunc
:http
se puede pasar implementaciones personalizadas deHandler
(por supuesto) pero también deCookieJar
oFile
.text/template
yhtml/template
pueden aceptar que las funciones sean accesibles desde las plantillas (en unFuncMap
).Exporte las funciones de acceso directo de su paquete si corresponde: en
http
, las personas que llaman pueden configurar y configurar por separado algunos objetoshttp.Server
o llamar ahttp.ListenAndServe(...)
que usa unServer
global. Eso le da un diseño agradable, todo está en un objeto y las personas que llaman pueden crear múltiplesServer
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 globalvar Conf map[string]interface{}
es útil. Pero tenga en cuenta las desventajas de la configuración global. Si desea escribir bibliotecas reutilizables, no pueden importarmyapp/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.). Sigoimports
con muchos paquetes de esta manera, tenemosgoimports
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 comoUser
yGroup
. Si viven en paquetes diferentes (un gran ''si''), no puede hacer queu.Groups()
devuelva un grupo[]group.Group
yg.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 otrainterface
que pueda acceder sinimport
un tipo de objeto específico. Dependiendo de su caso de uso, los tipos comoUser
yGroup
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.