循环依赖和接口 [英] Cyclic dependencies and interfaces
问题描述
我是一名长期的 Python 开发人员.我正在尝试 Go,将现有的 Python 应用程序转换为 Go.它是模块化的,非常适合我.
在 Go 中创建相同的结构后,我似乎陷入了循环导入错误,这比我想要的要多得多.在 python 中从来没有任何导入问题.我什至不必使用导入别名.所以我可能有一些在 python 中不明显的循环导入.我其实觉得这很奇怪.
无论如何,我迷路了,试图在 Go 中修复这些.我读过可以使用接口来避免循环依赖.但我不明白怎么做.我也没有找到任何关于这方面的例子.有人可以帮我吗?
目前的python应用结构如下:
/main.py/settings/routes.py 包含主要路由取决于 app1/routes.py、app2/routes.py 等/settings/database.py 函数,如 connect() 打开数据库会话/settings/constants.py 一般常量/apps/app1/views.py url 处理函数/apps/app1/models.py 应用程序特定的数据库功能取决于 settings/database.py/apps/app1/routes.py 应用特定路由/apps/app2/views.py url 处理函数/apps/app2/models.py 应用特定的数据库功能取决于 settings/database.py/apps/app2/routes.py 应用特定路由
settings/database.py
具有像 connect()
这样的通用函数,它打开一个数据库会话.因此,应用程序包中的应用程序调用 database.connect()
并打开一个 db 会话.
与 settings/routes.py
的情况相同,它具有允许应用程序将其子路由添加到主路由对象的功能.
设置包更多的是关于函数而不是数据/常量.这包含应用程序包中的应用程序使用的代码,否则必须在所有应用程序中复制这些代码.因此,如果我需要更改路由器类,例如,我只需要更改 settings/router.py
并且应用程序将继续工作而无需修改.
这有两个高级部分:确定哪些代码放在哪个包中,以及调整您的 API 以减少对包的需求尽可能多的依赖项.
关于设计无需某些导入的 API:
编写配置函数,以便在运行时而不是编译时将包相互连接起来.代替
routes
导入所有定义路由的包,它可以exportroutes.Register
,其中main
(或每个应用程序中的代码)可以调用.一般来说,配置信息可能流经main
或专用包;将其分散太多会使其难以管理.传递基本类型和
interface
值.如果您只依赖包的类型名称,也许您可以避免这种情况.也许某些处理[]Page
的代码可以改为使用文件名的[]string
或 ID 的[]int
或更通用的接口 (sql.Rows
) 代替.考虑拥有仅包含纯数据类型和接口的模式"包, 所以
User
与可能从数据库加载用户的代码是分开的.它不必依赖太多(可能依赖于任何东西),因此您可以从任何地方包含它.本·约翰逊在GopherCon 2016 建议并按依赖项组织包.
关于将代码组织成包:
作为一项规则,当每个部分都可以单独使用时,将其拆分.如果两个功能确实密切相关,则根本不必将它们拆分为包;您可以改为使用多个文件或类型进行组织.大包也可以;例如,Go 的
net/http
就是其中之一.按主题或依赖分解抓包包(
utils
、tools
).否则你最终可能会导入一个一个或两个功能的巨大utils
包(并承担其所有依赖项)(如果分离出来就不会有这么多依赖项).考虑将可重用代码向下"推入与您的特定用例分离的较低级别的包中.如果您有一个
包页面
内容管理系统和通用 HTML 操作代码,考虑将 HTML 内容向下"移动到package html
以便您可以使用它而无需导入不相关的内容管理内容.
在这里,我会重新安排一些东西,以便路由器不需要包含路由:相反,每个应用程序包都调用一个 router.Register()
方法.这就是 Gorilla Web 工具包的 mux
包 所做的.您的 routes
、database
和 constants
包听起来像是应该由您的应用代码导入而不是导入的低级片段.>
通常,尝试分层构建您的应用.您的上层、特定于用例的应用程序代码应该导入下层、更基础的工具,而不是相反.这里还有一些想法:
包有助于从调用者的角度分离独立可用的功能部分.对于内部代码组织,您可以轻松地在包中的源文件之间混洗代码.您在
x/foo.go
或x/bar.go
中定义的符号的初始命名空间只是包x
,并没有那么难根据需要拆分/合并文件,尤其是在goimports
等实用程序的帮助下.标准库的
net/http
大约有 7k 行(包括评论/空白但不包括测试).在内部,它被分成许多较小的文件和类型.但它是一个包,我认为因为用户没有理由想要,比如说,只是自己处理 cookie.另一方面,net
和net/url
是分开的,因为它们在 HTTP 之外使用.如果您可以将向下"实用程序推入独立的库中,并且感觉就像他们自己的精美产品,或者干净地将您的应用程序本身分层(例如,UI 位于 API 之上)核心库和数据模型).同样,横向"分离可以帮助您牢牢记住应用程序(例如,UI 层分解为用户帐户管理、应用程序核心和管理工具,或者比这更细粒度的东西).但是,核心点是,你可以自由地拆分或不拆分.
设置 API 以在运行时配置行为,这样您就不必在编译时导入它.因此,例如,您的 URL 路由器可以公开一个
注册
方法,而不是导入appA
、appB
等,并从每个方法读取var Routes
.你可以制作一个myapp/routes
包来导入router
和你所有的视图并调用router.Register
.基本思想是路由器是不需要导入应用程序视图的通用代码.组合配置 API 的一些方法:
通过
interface
s 或func
s 传递应用行为:http
可以传递自定义实现Handler
(当然)还有CookieJar
或File
.text/template
和html/template
可以接受可从模板访问的函数(在FuncMap
中).如果合适的话,从你的包中导出快捷功能:在
http
中,调用者可以制作和单独配置一些http.Server
对象,或调用使用全局Server
的http.ListenAndServe(...)
.这给了你一个很好的设计——一切都在一个对象中,调用者可以在一个进程中创建多个Server
等等——但它也提供了一种懒惰的方式来配置简单的单服务器案例.如果必须的话,只需用胶带粘住它:如果您的应用程序无法安装一个配置系统,您不必将自己限制在超级优雅的配置系统中:也许对于某些内容,带有全局
var Conf map[string]interface{}
的package "myapp/conf"
很有用.但请注意全局配置的缺点.如果你想写可重用的库,它们不能导入myapp/conf
;他们需要在构造函数等中接受他们需要的所有信息.全局变量也有硬连接的风险,假设某些东西在应用程序范围内始终具有单一值,但最终不会;也许今天你有一个单一的数据库配置或 HTTP 服务器配置等等,但有一天你没有.
移动代码或更改定义以减少依赖性问题的一些更具体的方法:
将基本任务与依赖于应用程序的任务分开.我用另一种语言开发的一个应用程序有一个utils"模块,将一般任务(例如,格式化日期时间或使用 HTML)与特定于应用程序的东西(取决于用户架构等).但是 users 包导入了 utils,创建了一个循环.如果我要移植到 Go,我会将依赖于用户的 utils 从 utils 模块向上"移出,也许是为了与用户代码一起使用,甚至在它之上.
考虑拆分抓包包. 稍微扩大最后一点:如果两个功能是独立的(也就是说,如果您将一些代码移到另一个包中,事情仍然有效) 和 从用户的角度来看无关,它们是被分成两个包的候选者.有时捆绑是无害的,但有时它会导致额外的依赖关系,或者不那么通用的包名称只会使代码更清晰.所以我上面的
utils
可能会被主题或依赖项分解(例如,strutil
、dbutil
等).如果你以这种方式得到了很多包,我们有goimports
帮助管理它们.将 API 中需要导入的对象类型替换为基本类型和
interface
s. 假设您应用中的两个实体具有多对多关系,例如User
s 和Group
s.如果它们位于不同的包中(一个很大的if"),您不能同时让u.Groups()
返回[]group.Group
和g.Users()
返回[]user.User
因为这需要包相互导入.但是,您可以更改其中一个或两个返回值,例如,ID 的
[]uint
或sql.Rows
或其他一些interface
你可以不用import
特定的对象类型.根据您的用例,像User
和Group
这样的类型可能密切相关,最好将它们放在一个包中,但如果您决定它们应该不同,这是一种方式.
感谢您的详细提问和跟进.
I am a long time python developer. I was trying out Go, converting an existing python app to Go. It is modular and works really well for me.
Upon creating the same structure in Go, I seem to land in cyclic import errors, a lot more than I want to. Never had any import problems in python. I never even had to use import aliases. So I may have had some cyclic imports which were not evident in python. I actually find that strange.
Anyways, I am lost, trying to fix these in Go. I have read that interfaces can be used to avoid cyclic dependencies. But I don't understand how. I didn't find any examples on this either. Can somebody help me on this?
The current python application structure is as follows:
/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
has generic functions like connect()
which opens a db session. So an app in the apps package calls database.connect()
and a db session is opened.
The same is the case with settings/routes.py
it has functions that allow apps to add their sub-routes to the main route object.
The settings package is more about functions than data/constants. This contains code that is used by apps in the apps package, that would otherwise have to be duplicated in all the apps. So if I need to change the router class, for instance, I just have to change settings/router.py
and the apps will continue to work with no modifications.
There're two high-level pieces to this: figuring out which code goes in which package, and tweaking your APIs to reduce the need for packages to take on as many dependencies.
On designing APIs that avoid the need for some imports:
Write config functions for hooking packages up to each other at run time rather than compile time. Instead of
routes
importing all the packages that define routes, it can exportroutes.Register
, whichmain
(or code in each app) can call. In general, configuration info probably flows throughmain
or a dedicated package; scattering it around too much can make it hard to manage.Pass around basic types and
interface
values. If you're depending on a package for just a type name, maybe you can avoid that. Maybe some code handling a[]Page
can get instead use a[]string
of filenames or a[]int
of IDs or some more general interface (sql.Rows
) instead.Consider having 'schema' packages with just pure data types and interfaces, so
User
is separate from code that might load users from the database. It doesn't have to depend on much (maybe on anything), so you can include it from anywhere. Ben Johnson gave a lightning talk at GopherCon 2016 suggesting that and organizing packages by dependencies.
On organizing code into packages:
As a rule, split a package up when each piece could be useful on its own. If two pieces of functionality are really intimately related, you don't have to split them into packages at all; you can organize with multiple files or types instead. Big packages can be OK; Go's
net/http
is one, for instance.Break up grab-bag packages (
utils
,tools
) by topic or dependency. Otherwise you can end up importing a hugeutils
package (and taking on all its dependencies) for one or two pieces of functionality (that wouldn't have so many dependencies if separated out).Consider pushing reusable code 'down' into lower-level packages untangled from your particular use case. If you have a
package page
containing both logic for your content management system and all-purpose HTML-manipulation code, consider moving the HTML stuff "down" to apackage html
so you can use it without importing unrelated content management stuff.
Here, I'd rearrange things so the router doesn't need to include the routes: instead, each app package calls a router.Register()
method. This is what the Gorilla web toolkit's mux
package does. Your routes
, database
, and constants
packages sound like low-level pieces that should be imported by your app code and not import it.
Generally, try to build your app in layers. Your higher-layer, use-case-specific app code should import lower-layer, more fundamental tools, and never the other way around. Here are some more thoughts:
Packages are good for separating independently usable bits of functionality from the caller's perspective. For your internal code organization, you can easily shuffle code between source files in the package. The initial namespace for symbols you define in
x/foo.go
orx/bar.go
is just packagex
, and it's not that hard to split/join files as needed, especially with the help of a utility likegoimports
.The standard library's
net/http
is about 7k lines (counting comments/blanks but not tests). Internally, it's split into many smaller files and types. But it's one package, I think 'cause there was no reason users would want, say, just cookie handling on its own. On the other hand,net
andnet/url
are separate because they have uses outside HTTP.It's great if you can push "down" utilities into libraries that are independent and feel like their own polished products, or cleanly layer your application itself (e.g., UI sits atop an API sits atop some core libraries and data models). Likewise "horizontal" separation may help you hold the app in your head (e.g., the UI layer breaks up into user account management, the application core, and administrative tools, or something finer-grained than that). But, the core point is, you're free to split or not as works for you.
Set up APIs to configure behavior at run-time so you don't have to import it at compile time. So, for example, your URL router can expose a
Register
method instead of importingappA
,appB
, etc. and reading avar Routes
from each. You could make amyapp/routes
package that importsrouter
and all your views and callsrouter.Register
. The fundamental idea is that the router is all-purpose code that needn't import your application's views.Some ways to put together config APIs:
Pass app behavior via
interface
s orfunc
s:http
can be passed custom implementations ofHandler
(of course) but alsoCookieJar
orFile
.text/template
andhtml/template
can accept functions to be accessible from templates (in aFuncMap
).Export shortcut functions from your package if appropriate: In
http
, callers can either make and separately configure somehttp.Server
objects, or callhttp.ListenAndServe(...)
that uses a globalServer
. That gives you a nice design--everything's in an object and callers can create multipleServer
s in a process and such--but it also offers a lazy way to configure in the simple single-server case.If you have to, just duct-tape it: You don't have to limit yourself to super-elegant config systems if you can't fit one to your app: maybe for some stuff a
package "myapp/conf"
with a globalvar Conf map[string]interface{}
is useful. But be aware of downsides to global conf. If you want to write reusable libraries, they can't importmyapp/conf
; they need to accept all the info they need in constructors, etc. Globals also risk hard-wiring in an assumption something will always have a single value app-wide when it eventually won't; maybe today you have a single database config or HTTP server config or such, but someday you don't.
Some more specific ways to move code or change definitions to reduce dependency issues:
Separate fundamental tasks from app-dependent ones. One app I work on in another language has a "utils" module mixing general tasks (e.g., formatting datetimes or working with HTML) with app-specific stuff (that depends on the user schema, etc.). But the users package imports the utils, creating a cycle. If I were porting to Go, I'd move the user-dependent utils "up" out of the utils module, maybe to live with the user code or even above it.
Consider breaking up grab-bag packages. Slightly enlarging on the last point: if two pieces of functionality are independent (that is, things still work if you move some code to another package) and unrelated from the user's perspective, they're candidates to be separated into two packages. Sometimes the bundling is harmless, but other times it leads to extra dependencies, or a less generic package name would just make clearer code. So my
utils
above might be broken up by topic or dependency (e.g.,strutil
,dbutil
, etc.). If you wind up with lots of packages this way, we've gotgoimports
to help manage them.Replace import-requiring object types in APIs with basic types and
interface
s. Say two entities in your app have a many-to-many relationship likeUser
s andGroup
s. If they live in different packages (a big 'if'), you can't have bothu.Groups()
returning a[]group.Group
andg.Users()
returning[]user.User
because that requires the packages to import each other.However, you could change one or both of those return, say, a
[]uint
of IDs or asql.Rows
or some otherinterface
you can get to withoutimport
ing a specific object type. Depending on your use case, types likeUser
andGroup
might be so intimately related that it's better just to put them in one package, but if you decide they should be distinct, this is a way.
Thanks for the detailed question and followup.
这篇关于循环依赖和接口的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!