一尘不染

循环依赖关系和接口

go

我是一个长期的python开发人员。我正在尝试Go,将现有的python应用程序转换为Go。它是模块化的,对我来说真的很好用。

在Go中创建相同的结构后,我似乎陷入了周期性的导入错误,这比我想要的要多得多。从未在python中出现任何导入问题。我什至不必使用导入别名。所以我可能有一些在python中不明显的周期性导入。我实际上发现那个奇怪。

无论如何,我迷路了,试图在Go中修复这些问题。我读过接口可以用来避免循环依赖。但是我不知道如何。我也没有找到任何例子。有人可以帮我吗?

当前的python应用程序结构如下:

/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具有诸如connect()打开数据库会话之类的通用功能。因此,应用程序包中的一个应用程序将调用database.connect()并打开数据库会话。

settings/routes.py它具有允许应用程序将其子路线添加到主路线对象的功能的情况也是如此。

设置包更多地是关于功能而不是数据/常量。它包含应用程序包中的应用程序使用的代码,否则必须在所有应用程序中重复。因此,例如,如果我需要更改路由器类,则只需更改settings/router.py,应用程序将继续运行而无需进行任何修改。


阅读 284

收藏
2020-07-02

共1个答案

一尘不染

这有两个高层次的内容:找出哪个代码放入哪个包中,以及调整API以减少包承担尽可能多的依赖关系的需求。

在设计避免某些导入的API时:

  • 编写配置函数,以便 在运行时(而不是在编译时) 将程序包相互挂钩。routes它可以 导入 routes.Register,而不是导入所有定义路由的包,而main可以调用(或每个应用程序中的代码)。通常,配置信息可能会流经main一个专用软件包或专用软件包。分散太多会使其难以管理。

  • _传递基本类型和interface值。_如果仅依靠包来获取类型名,则可以避免这种情况。也许一些处理a的代码[]Page可以改为使用[]string文件名或[]intID或更通用的接口(sql.Rows)。

  • 考虑具有仅包含纯数据类型和接口的“ schema”包, 因此User与可能从数据库加载用户的代码分开。它不必依赖太多(也许任何东西),因此您可以在任何地方添加它。本·约翰逊(Ben Johnson)在GopherCon 2016上发表了闪电般的演讲,提出了建议,并通过依赖项来组织软件包。

在将代码组织到包中时:

  • 通常, 当每件物品都可以单独使用时 ,请 拆分包装 。如果两个功能确实紧密相关,那么您根本就不必将它们拆分为多个程序包。您可以改为组织多个文件或类型。大包装可以。net/http例如,围棋是一个。

  • _分手抓斗袋包装(utilstools按主题或依赖)。_否则,您最终可能会utils为一个或两个功能(如果分离出来就不会有那么多的依赖关系)导入一个巨大的程序包(并承担所有依赖关系)。

  • 考虑将可重用的代码“向下”推送到与您的特定用例无关的较低级包中。 如果您既package page包含内容管理系统的逻辑,又包含通用的HTML操纵代码,请考虑将HTML内容“向下”移动到a,package html以便可以使用它而无需导入无关的内容管理内容。


在这里,我将重新安排事情,以便路由器不需要包括路由:相反,每个应用程序包都调用一个router.Register()方法。这就是Gorilla网络工具mux包的功能。您的routesdatabaseconstants包听起来像是低级片段,应该由您的应用程序代码导入而不是导入。

通常,尝试分层构建应用程序。您的上层,特定于用例的应用程序代码应导入下层,更基础的工具,而绝不能相反。这里还有一些想法:

  • __从 调用者的 角度来看, 对于将 独立可用的功能部分 分开是很有 。对于 内部 代码组织,您可以轻松地在包中的源文件之间对代码进行混洗。您在x/foo.go或中定义的符号的初始名称空间x/bar.go只是package x,根据需要拆分/合并文件并不难,尤其是借助实用程序的帮助goimports

标准库net/http大约有7k行(计数注释/空白但不包含测试)。在内部,它分为许多较小的文件和类型。但这是一个程序包,我认为这是因为用户没有理由只想单独处理cookie。另一方面,netnet/url
分开的,因为它们具有HTTP外部的用法。

如果您 可以
将“实用程序”下推到独立的库中并感觉像它们自己的精美产品,或者干净地将应用程序本身分层(例如,UI位于API之上,某些核心库和数据模型之上),则非常好。同样,“水平”分隔可以帮助您将应用程序牢牢掌握(例如,UI层分为用户帐户管理,应用程序核心和管理工具,或者比它更细的东西)。但是,核心是,
您可以自由拆分也可以不拆分

  • 设置API以便在运行时配置行为,因此您不必在编译时将其导入。 因此,举例来说,您的网址路由器可以公开一个Register方法,而不是进口appAappB等等,读一var Routes从每个。您可以制作一个myapp/routes包,以导入router所有视图和调用router.Register。基本思想是路由器是通用代码,不需要导入应用程序的视图。

组合配置API的一些方法:

* _通过传递行为的应用程序`interface`S或`func`S:_ `http`可以传递定制实现`Handler`(当然),但也`CookieJar`还是`File`。`text/template`并`html/template`可以接受可从模板访问的功能(在中`FuncMap`)。

* _如果合适_`http`, _从包中导出快捷方式功能:_ 在中,调用者可以创建并分别配置某些`http.Server`对象,也`http.ListenAndServe(...)`可以使用global进行调用`Server`。这就给了你一个不错的设计-一切都在一个对象,调用者可以创建多个`Server`在一个过程,例如秒-但它 _也_ 提供了一个懒惰的方式来配置在简单的单台服务器的情况。

* _如果必须的话,只需_ 做一下 _录音即可:_ 如果您无法在自己的应用程序中安装一个超级优雅的配置系统,则不必将自己局限于超级优雅的配置系统:也许对于某些`package "myapp/conf"`具有全局性的东西`var Conf map[string]interface{}`很有用。但是要注意全球会议的弊端。如果要编写可重用的库,则不能导入`myapp/conf`;他们需要接受构造函数中所需的所有信息,等等。全局变量还冒着一个艰难的假设,即某些东西最终在应用程序范围内始终只有一个值。也许今天您只有一个数据库配置或HTTP服务器配置等,但是有一天您没有。

一些更具体的方法来移动代码或更改定义以减少依赖性问题:

  • 将基本任务与依赖于应用程序的任务分开。 我使用另一种语言处理的一个应用程序具有一个“ utils”模块,将常规任务(例如,格式化日期时间或使用HTML)与特定于应用程序的内容(取决于用户架构等)混合在一起。但是用户包会导入utils,从而创建一个循环。如果要移植到Go,我会将“用户”依赖的utils“向上”移出utils模块,也许与用户代码一起使用甚至更高。

  • 考虑拆开手提袋包装。 最后一点:如果两个功能是独立的(也就是说,如果将某些代码移到另一个程序包中,功能仍然有效) 并且 与用户的观点无关,则可以将它们分成两个程序包。有时捆绑是无害的,但是有时捆绑会导致额外的依赖关系,或者通用性较低的软件包名称只会使代码更清晰。所以我utils上面可能会按主题或依赖被分解(如strutildbutil等)。如果您以这种方式获得大量软件包,我们goimports将帮助您进行管理。

  • 用基本类型和替换API中需要导入的对象类型interface假设您应用中的两个实体具有Users和Groups 的多对多关系。如果它们位于不同的包中(大的“ if”),则不能同时u.Groups()返回a []group.Groupg.Users()return,[]user.User因为这要求包彼此导入。

但是,您可以更改这些返回值中的一个或两个,例如[]uintID或ID,或a
sql.Rows或其他一些返回值,interface而无需import指定特定的对象类型。根据您的用例,像UserGroup这样的类型可能紧密相关,以至于最好将它们放在一个包中,但是如果您决定它们应该是不同的,这是一种方法。

感谢您的详细问题和跟进。

2020-07-02