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

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

问题描述

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

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.

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

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.

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

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?

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

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具有类似于connect()的通用功能,可以打开数据库会话.因此,应用程序包中的一个应用程序调用database.connect()并打开了数据库会话.

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.

settings/routes.py的情况与此相同,它具有允许应用程序将其子路线添加到主路线对象的功能.

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.

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

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.

推荐答案

这有两个高层次的内容:找出哪个代码放入哪个程序包中,并调整API以减少使用程序包的需要尽可能多的依赖项.

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.

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

On designing APIs that avoid the need for some imports:

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

  • 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.

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

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.

考虑具有仅包含纯数据类型和接口的"schema"软件包,因此User与可能从数据库中加载用户的代码分开.它不必依赖太多(也许任何东西),因此您可以在任何地方添加它. 本·约翰逊在GopherCon 2016 提出建议并通过依赖项组织软件包.

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:

  • 通常,当每件物品都可能有用时,将它们分开包装.如果两个功能确实紧密相关,那么您根本不必将它们拆分为多个程序包.您可以改用多个文件或类型进行组织.大包装可以的;例如,Go的net/http是一个.

  • 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.

按主题或依存关系分解抓包软件包(utilstools).否则,您最终可能会导入庞大的utils软件包(并承担所有任务)一种或两种功能(如果分离出来就不会有那么多的依赖关系).

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).

考虑将可重用的代码向下"推送到与特定用例无关的较低级软件包中.如果您的package page既包含内容管理系统的逻辑,又包含所有内容,用途的HTML操纵代码,请考虑将HTML内容向下"移动到package html,以便可以在不导入无关内容管理内容的情况下使用它.

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.

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

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:

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

  • 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.

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

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.

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

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.

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

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.

一些组合配置API的方法:

Some ways to put together config APIs:

  • 通过interface s或func s传递应用程序行为: http可以传递Handler的自定义实现(当然),也可以传递CookieJarFile. text/templatehtml/template可以接受可从模板(在FuncMap中)访问的功能.

  • 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).

在适当的情况下从软件包中导出快捷方式功能:http中,调用者可以制作并分别配置某些http.Server对象,或者调用使用全局Server.这为您提供了一个不错的设计-一切都在一个对象中,调用者可以在一个进程中创建多个Server,并且如此-但它 还提供了一种在简单的单服务器中进行配置的惰性方法案子.

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.

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

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:

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

  • 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.

考虑将抓包打包.最后一点:如果两个功能是独立的(也就是说,如果您将一些代码移到另一个包中,事情仍然可行) )从用户的角度来看无关,它们是被分为两个包的候选对象.有时捆绑是无害的,但是有时捆绑会导致额外的依赖关系,或者通用性较低的软件包名称只会使代码更清晰.因此,我的上述utils可能会按主题或依存关系(例如strutildbutil等)分解.如果您通过这种方式获得大量软件包,则可以使用 goimports 来帮助管理它们.

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.

使用基本类型和interface替换API中需要导入的对象类型.假设您应用中的两个实体具有多对多关系,例如UserGroup s.如果它们生活在不同的软件包中(很大的"if"),则不能同时使u.Groups()返回[]group.Groupg.Users()返回[]user.User,因为这要求这些软件包彼此导入.

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.

但是,您可以更改返回的一个或两个,例如ID的[]uintsql.Rows或其他一些interface,而无需import特定的对象类型.根据您的用例,像UserGroup这样的类型可能联系紧密,以至于最好将它们放在一个包中,但是如果您决定将它们区别开,这是一种方法.

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天全站免登陆