循环依赖和接口 [英] Cyclic dependencies and interfaces

查看:28
本文介绍了循环依赖和接口的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我是一名长期的 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 导入所有定义路由的包,它可以export routes.Register,其中 main(或每个应用程序中的代码)可以调用.一般来说,配置信息可能流经 main 或专用包;将其分散太多会使其难以管理.

  • 传递基本类型和interface 值.如果您只依赖包的类型名称,也许您可​​以避免这种情况.也许某些处理 []Page 的代码可以改为使用文件名的 []string 或 ID 的 []int 或更通用的接口 (sql.Rows) 代替.

  • 考虑拥有仅包含纯数据类型和接口的模式"包, 所以 User 与可能从数据库加载用户的代码是分开的.它不必依赖太多(可能依赖于任何东西),因此您可以从任何地方包含它.本·约翰逊在GopherCon 2016 建议并按依赖项组织包.

关于将代码组织成包:

  • 作为一项规则,当每个部分都可以单独使用时,将其拆分.如果两个功能确实密切相关,则根本不必将它们拆分为包;您可以改为使用多个文件或类型进行组织.大包也可以;例如,Go 的 net/http 就是其中之一.

  • 按主题或依赖分解抓包包(utilstools).否则你最终可能会导入一个一个或两个功能的巨大 utils 包(并承担其所有依赖项)(如果分离出来就不会有这么多依赖项).

  • 考虑将可重用代码向下"推入与您的特定用例分离的较低级别的包中.如果您有一个 包页面内容管理系统和通用 HTML 操作代码,考虑将 HTML 内容向下"移动到 package html 以便您可以使用它而无需导入不相关的内容管理内容.

<小时>

在这里,我会重新安排一些东西,以便路由器不需要包含路由:相反,每个应用程序包都调用一个 router.Register() 方法.这就是 Gorilla Web 工具包的 mux 所做的.您的 routesdatabaseconstants 包听起来像是应该由您的应用代码导入而不是导入的低级片段.

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

  • 有助于从调用者的角度分离独立可用的功能部分.对于内部代码组织,您可以轻松地在包中的源文件之间混洗代码.您在 x/foo.gox/bar.go 中定义的符号的初始命名空间只是包 x,并没有那么难根据需要拆分/合并文件,尤其是在 goimports 等实用程序的帮助下.

    标准库的 net/http 大约有 7k 行(包括评论/空白但不包括测试).在内部,它被分成许多较小的文件和类型.但它是一个包,我认为因为用户没有理由想要,比如说,只是自己处理 cookie.另一方面,netnet/url 是分开的,因为它们在 HTTP 之外使用.

    如果您可以将向下"实用程序推入独立的库中,并且感觉就像他们自己的精美产品,或者干净地将您的应用程序本身分层(例如,UI 位于 API 之上)核心库和数据模型).同样,横向"分离可以帮助您牢牢记住应用程序(例如,UI 层分解为用户帐户管理、应用程序核心和管理工具,或者比这更细粒度的东西).但是,核心点是,你可以自由地拆分或不拆分.

  • 设置 API 以在运行时配置行为,这样您就不必在编译时导入它.因此,例如,您的 URL 路由器可以公开一个 注册 方法,而不是导入appAappB 等,并从每个方法读取var Routes.你可以制作一个 myapp/routes 包来导入 router 和你所有的视图并调用 router.Register.基本思想是路由器是不需要导入应用程序视图的通用代码.

    组合配置 API 的一些方法:

    • 通过 interfaces 或 funcs 传递应用行为: http 可以传递自定义实现Handler(当然)还有 CookieJarFile.text/templatehtml/template 可以接受可从模板访问的函数(在 FuncMap 中).

    • 如果合适的话,从你的包中导出快捷功能:http中,调用者可以制作和单独配置一些http.Server对象,或调用使用全局 Serverhttp.ListenAndServe(...).这给了你一个很好的设计——一切都在一个对象中,调用者可以在一个进程中创建多个 Server 等等——但它提供了一种懒惰的方式来配置简单的单服务器案例.

    • 如果必须的话,只需用胶带粘住它:如果您的应用程序无法安装一个配置系统,您不必将自己限制在超级优雅的配置系统中:也许对于某些内容,带有全局 var Conf map[string]interface{}package "myapp/conf" 很有用.但请注意全局配置的缺点.如果你想写可重用的库,它们不能导入myapp/conf;他们需要在构造函数等中接受他们需要的所有信息.全局变量也有硬连接的风险,假设某些东西在应用程序范围内始终具有单一值,但最终不会;也许今天你有一个单一的数据库配置或 HTTP 服务器配置等等,但有一天你没有.

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

  • 将基本任务与依赖于应用程序的任务分开.我用另一种语言开发的一个应用程序有一个utils"模块,将一般任务(例如,格式化日期时间或使用 HTML)与特定于应用程序的东西(取决于用户架构等).但是 users 包导入了 utils,创建了一个循环.如果我要移植到 Go,我会将依赖于用户的 utils 从 utils 模块向上"移出,也许是为了与用户代码一起使用,甚至在它之上.

  • 考虑拆分抓包包. 稍微扩大最后一点:如果两个功能是独立的(也就是说,如果您将一些代码移到另一个包中,事情仍然有效) 从用户的角度来看无关,它们是被分成两个包的候选者.有时捆绑是无害的,但有时它会导致额外的依赖关系,或者不那么通用的包名称只会使代码更清晰.所以我上面的 utils 可能会被主题或依赖项分解(例如,strutildbutil 等).如果你以这种方式得到了很多包,我们有 goimports帮助管理它们.

  • 将 API 中需要导入的对象类型替换为基本类型和 interfaces. 假设您应用中的两个实体具有多对多关系,例如Users 和 Groups.如果它们位于不同的包中(一个很大的if"),您不能同时让 u.Groups() 返回 []group.Groupg.Users() 返回 []user.User 因为这需要包相互导入.

    但是,您可以更改其中一个或两个返回值,例如,ID 的 []uintsql.Rows 或其他一些 interface 你可以不用 import 特定的对象类型.根据您的用例,像 UserGroup 这样的类型可能密切相关,最好将它们放在一个包中,但如果您决定它们应该不同,这是一种方式.

感谢您的详细提问和跟进.

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 export routes.Register, which main (or code in each app) can call. In general, configuration info probably flows through main 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 huge utils 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 a package 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 or x/bar.go is just package x, and it's not that hard to split/join files as needed, especially with the help of a utility like goimports.

    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 and net/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 importing appA, appB, etc. and reading a var Routes from each. You could make a myapp/routes package that imports router and all your views and calls router.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 interfaces or funcs: http can be passed custom implementations of Handler (of course) but also CookieJar or File. text/template and html/template can accept functions to be accessible from templates (in a FuncMap).

    • Export shortcut functions from your package if appropriate: In http, callers can either make and separately configure some http.Server objects, or call http.ListenAndServe(...) that uses a global Server. That gives you a nice design--everything's in an object and callers can create multiple Servers 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 global var Conf map[string]interface{} is useful. But be aware of downsides to global conf. If you want to write reusable libraries, they can't import myapp/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 got goimports to help manage them.

  • Replace import-requiring object types in APIs with basic types and interfaces. Say two entities in your app have a many-to-many relationship like Users and Groups. If they live in different packages (a big 'if'), you can't have both u.Groups() returning a []group.Group and g.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 a sql.Rows or some other interface you can get to without importing a specific object type. Depending on your use case, types like User and Group 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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆