我们目前在一个项目中有很多类,每个类都实现一个接口,主要是出于DI的原因。
现在,我个人感觉这些接口应该放在同一个程序集中的一个单独的命名空间中(所以我们有一个MyCompany.CoolApp.DataAccess
程序集,并且在其中有一个Interfaces
命名空间给出{{1 }})。
然而,有人建议这些接口实际上应该在他们自己的程序集中。我的问题是 - 他们是对的吗?我可以看到有一些好处(例如,其他项目只需要使用接口程序集),但在一天结束时,所有这些程序集都需要加载。在我看来,可能会有一个稍微复杂的部署问题,因为Visual Studio不会自动将实现程序集拉入目标的bin文件夹。
是否有针对此的最佳做法指南?
编辑:
让我的观点更加清晰:我们已经将UI,DataAccess,DataModel和其他东西分成不同的程序集。我们当前也可以毫不费力地将我们的实现换成不同的实现,因为我们使用Unity(IOC框架)将实现类映射到接口。我应该指出,除了多态性的原因和为单元测试创建模拟之外,我们从不编写同一接口的两个实现。因此,除了单元测试之外,我们目前没有“换出”实现。
我看到在与实现相同的程序集中使用接口的唯一缺点是将加载整个程序集(包括未使用的实现)。
但是,我可以看到,将它们放在不同的程序集中意味着开发人员不会意外地“新”实现类而不是使用IOC包装器创建它。我从答案中无法理解的一点是部署问题。如果我只是依赖于接口程序集,我会有类似以下结构:
MyCompany.CoolApp.DataAccess.Interfaces
当我构建它时,自动放入bin文件夹的程序集就是那些接口程序集。但是,我在Unity中的类型映射将不同的接口映射到它们的实际实现。包含我的实现的程序集如何最终在bin文件夹中?
答案 0 :(得分:26)
通常期望?的做法是将它们放在自己的程序集中,因为那时使用这些接口的给定项目不需要硬实现的实现那些接口。从理论上讲,这意味着你可以在很少或没有痛苦的情况下更换实现。
那就是说,我不记得我上次这样做了,对于@ David_001来说,这不一定是“通常的”。我们倾向于使我们的接口与实现一致,这是我们对正在测试的接口的最常见用途。
我认为根据你的作品有不同的立场。我倾向于生成LOB应用程序,这些应用程序需要在内部与其他应用程序和团队进行互操作,因此任何给定应用程序的公共API都有一些利益相关者。但是,这并不像为许多未知客户端生成库或框架那样极端,因为公共API突然变得更加重要。
在部署方案中,如果您更改了实现,理论上您可以部署该单个DLL - 例如,仅留下UI和接口DLL。如果您一起编译了接口和实现,则可能需要重新部署UI DLL ...
另一个好处是代码的清晰隔离 - 有一个接口(或共享库)DLL明确地向开发团队中任何放置新类型等的任何人说明。我不再计算这是一个好处,因为我们没有任何问题没有这样做,无论接口放在何处,公共合同仍然很容易找到。
我不知道是否有最佳实践可用或反对,重要的是,在代码中,您总是在使用接口,并且永远不会让任何代码泄漏到使用实现。
答案 1 :(得分:14)
到目前为止,答案似乎都说将接口放在自己的程序集中是“通常”的做法。我不同意将不相关的接口放入一个“共享”公共程序集中,因此这意味着我需要为每个“实现”程序集安装一个接口程序集。
但是,考虑到这一点,我想不出这个实践的许多真实世界的例子(例如,做log4net或NUnit提供公共接口程序集,以便消费者可以决定不同的实现?如果是这样,我可以使用nunit的其他实现吗?)。通过谷歌花费了很多年,我找到了很多资源。
单独的组件是否意味着松耦合?以下建议不:
http://www.theserverside.net/tt/articles/showarticle.tss?id=ControllingDependencies
http://codebetter.com/blogs/jeremy.miller/archive/2008/09/30/separate-assemblies-loose-coupling.aspx
我可以通过Google搜索找到的普遍共识是更少的程序集更好,除非有一个非常好的理由添加新的程序集。另见:
http://www.cauldwell.net/patrick/blog/ThisIBelieveTheDeveloperEdition.aspx
由于我没有生成公共API,并且我已经将接口放入他们自己的命名空间中,因此盲目地创建新程序集 是有道理的。这种方法的好处似乎超过了添加更多组件的潜在好处(我不太可能实际获得这些好处)。
答案 2 :(得分:9)
我称之为共享类型(我也使用DI)的模式是有一个单独的程序集,其中包含以下应用程序级概念(而不是常见程序集中的常见概念):
通过这种方式,可以管理客户端和核心应用程序库之间的依赖关系,因为客户端不能直接依赖于具体实现,也不能作为添加直接程序集引用然后访问任何旧公共类型的意外后果。< / p>
然后我有一个运行时类型设计,我在应用程序启动时设置我的DI容器,或者开始一套单元测试。通过这种方式,实现与我如何通过DI改变它们之间存在明显的分离。我的客户端模块永远不会直接引用实际的核心库,只能引用“SharedTypes”库。
我的设计的关键是为客户端(无论是WPF应用程序还是NUnit)设置一个公共运行时概念,它设置了所需的依赖关系,即具体实现或某种模拟/存根。
如果没有考虑上述共享类型,而是客户端使用具体实现添加对程序集的引用,那么客户端很容易使用具体实现而不是接口,显而易见和非明显的方法。随着时间的推移逐渐结束过度耦合是非常容易的,这几乎不可能在没有大量努力和更重要的时间的情况下解决。
<强>更新强>
通过示例说明依赖关系如何在目标应用程序中结束。
在我的情况下,我有一个WPF客户端应用程序。我使用Prism和Unity(用于DI),其中重要的是,Prism用于应用程序组合。
使用Prism,您的应用程序组件只是一个Shell,功能的实际实现驻留在“模块”程序集中(您可以为每个概念模块使用单独的程序集,但这不是必需的,我有一个模块程序集ATM)。 shell负责加载模块 - 这些模块的组成 应用程序。模块使用SharedTypes程序集,但shell引用具体程序集。我讨论的运行时类型设计负责初始化依赖关系,这在Shell中完成。
以这种方式,具有所有功能的模块组件不依赖于具体实现。它们由shell加载,它将依赖项排序。 shell引用了具体的程序集,这就是它们进入bin目录的方式。依赖草图:
Shell.dll <-- Application
--ModuleA.dll
--ModuleB.dll
--SharedTypes.dll
--Core.dll
--Common.dll + Unity.dll <-- RuntimeDI
ModuleA.dll
--SharedTypes.dll
--Common.dll + Unity.dll <-- RuntimeDI
ModuleB.dll
--SharedTypes.dll
--Common.dll + Unity.dll <-- RuntimeDI
SharedTypes.dll
--...
答案 3 :(得分:6)
我同意勾选答案。大卫,对你有好处。事实上,我很高兴看到答案,以为我疯了。
我总是在企业C#自由职业者的工作中看到这个有趣的“笔盆”模式,人们遵循人群的惯例,团队必须遵守,不符合就会制造麻烦。
另一个疯狂的是每个程序集废话的一个命名空间。所以你得到一个SomeBank.SomeApp.Interfaces
命名空间,所有都在其中。
对我来说,这意味着类型分散在名称空间和程序集中,包含一大堆我不关心的东西必须在整个地方被引用。
至于接口,我甚至不在我的私人应用程序中使用接口; DI适用于类型,具体的虚拟,基类或接口。我相应地选择并根据它们的作用在DLL中放置类型。
我以后从未遇到过DI或交换逻辑的问题。
•.NET程序集是安全性,API范围和部署的一个单元,并且与名称空间无关。
•如果两个程序集相互依赖,则它们不能单独部署和版本化,应该合并。
•拥有许多DLL通常意味着将大量内容公开,因此很难将必须公开的类型成员告知实际的公共API,因为它们被任意放入自己的程序集中。
•我的DLL之外的代码是否需要使用我的类型?
•开始保守;我通常可以轻松地将一个类型移出一个图层,但另一方面则更难。
•我可以将我的功能区域或框架整齐地打包到NuGet包中,使其完全可选且可版本化,就像任何其他包一样吗?
•我的类型是否与特征的交付一致?是否可以将它们放在特征命名空间中?
•许多真正的库和框架都有品牌,使它们易于讨论,并且它们不会烧毁暗示其使用或含糊不清的命名空间名称,我可以使用像Steelcore这样的“代码名称”来标记我的应用程序的组件而不是通用的陈词滥调和令人困惑的术语,错误的'服务'?
修改强>
这是我今天在开发中看到的一个被误解的事情。太糟糕了。
您有一个API,因此将其所有类型放在单个API项目中。只有在需要共享/重用它们时才将它们移出。当您将它们移出时,将它们直接移动到NuGet包中,该包具有带有包的意图和焦点的清晰名称。如果你正在努力争取一个名字,并考虑“共同”,那可能是因为你正在创建一个倾销场。
您应该将NuGet包计入一系列相关包。您的“核心”包应该对其他包具有最小的依赖性。里面的类型与使用有关,并且相互依赖。
然后,您需要为需要其他依赖项集的更专业的类型和子类型创建一个新包。更清楚:您通过外部依赖关系拆分库,而不是通过类型或类型接口或异常。
所以你可能会把所有类型都放在一个大的库中,但是一些更专业的类型(有色点)依赖于某些外部库,所以现在你的库需要引入所有这些依赖项。这是不必要的,您应该将这些类型分解为更多专门的库,这些库确实采用了所需的依赖项。
包A和B中的类型可以属于相同的命名空间。引用A引入一组类型,然后可选地引用B补充命名空间的更多。
就是这样。
路
答案 4 :(得分:1)
最近,我一直主张将接口与实现分开。
即使团队中有人说“这些接口的 99% 的实现永远不会改变”,也不要说永远不会。
最近将大型项目从 EntityFramework
移至 EntityFrameworkCore
时,拆分库保存了我们的重构。我们刚刚使用 EntityFramework
更改了 10 个项目中的实现,然后去喝咖啡。
答案 5 :(得分:0)
我在对象浏览器中查看System.Data.dll(4.0),我可以看到它本身不仅具有接口,而且还包括所有工具类,如DataSet,DataTable,DataRow,DataColumn等。此外,浏览它所拥有的命名空间列表,如System.Data,System.Data.Common,System.Configuration&amp; System.Xml,它建议首先让接口包含在它们自己的程序集中,所有相关和必需的代码保持在一起,其次,更重要的是在整个应用程序(或框架)中重复使用相同的名称空间,以便实际上分离类。 / p>
答案 6 :(得分:0)
我知道这个线程确实非常古老,但是对此我有一个想法,我想把它放在那里。
我得到了“重用”的程序集。但是我们不能对我们的特定解决方案走得更远吗?
如果我们的SOLUTION中有一个具有适当接口的程序集,则可以构建该程序集并在有意义的任何地方使用它,包括重用。
但是,对于我们SAME解决方案中的其他项目,为什么不简单地通过LINK将接口文件添加到需要定义接口的其他项目中呢?
这样做,将简化您特定项目的部署(不需要部署接口程序集)。另一方面,如果要在其他解决方案中重用接口,则可以选择复制接口文件,或仅引用接口程序集。
似乎两全其美。我们可以选择如何获取该界面,并且该界面仍受版本控制。
坦率