我应该有一个单独的接口程序集吗?

Dav*_*001 51 .net assemblies namespaces

我们目前在一个项目中有很多类,每个类都实现一个接口,主要是出于DI的原因.

现在,我个人的感觉是这些接口应该放在同一个程序集中的一个单独的命名空间中(所以我们有一个MyCompany.CoolApp.DataAccess程序集,并且在其中有一个Interfaces命名空间给出 MyCompany.CoolApp.DataAccess.Interfaces).

但是,有人建议这些接口实际上应该在它们自己的程序集中.我的问题是 - 他们是对的吗?我可以看到有一些好处(例如,其他项目只需要使用接口程序集),但在一天结束时,所有这些程序集都需要加载.在我看来,可能会有一个稍微复杂的部署问题,因为Visual Studio不会自动将实现程序集拉入目标的bin文件夹.

是否有针对此的最佳实践指南?

编辑:

为了使我的观点更清楚:我们已经将UI,DataAccess,DataModel和其他东西分成不同的程序集.我们当前也可以毫不费力地将我们的实现换成不同的实现,因为我们使用Unity(IOC框架)将实现类映射到接口.我应该指出,除了多态性的原因和为单元测试创​​建模拟之外,我们从不编写同一接口的两个实现.因此,除了单元测试之外,我们目前没有"换出"实现.

我看到在与实现相同的程序集中使用接口的唯一缺点是将加载整个程序集(包括未使用的实现).

但是,我可以看到将它们放在不同的程序集中意味着开发人员不会意外地"新"实现类而不是使用IOC包装器创建它.

我从答案中无法理解的一点是部署问题.如果我只是依赖于接口程序集,我会有类似以下结构的东西:

MyCompany.MyApplication.WebUI
    References:
        MyCompany.MyApplication.Controllers.Interfaces
        MyCompany.MyApplication.Bindings.Interfaces
        etc...
Run Code Online (Sandbox Code Playgroud)

当我构建它时,自动放入bin文件夹的程序集就是那​​些接口程序集.但是,我在单元格中的类型映射将不同的接口映射到它们的实际实现.包含我的实现的程序集如何最终在bin文件夹中?

Ada*_*rth 27

通常的预期实践是将它们放在它们自己的程序集中,因为使用这些接口的给定项目不需要对这些接口的实现的硬引用.从理论上讲,这意味着你可以在很少或没有痛苦的情况下交换实现.

也就是说,我不记得我上次这样做了,@ David_001的观点不一定是"通常的".我们倾向于使我们的接口与实现一致,这是我们对正在测试的接口的最常见用途.

我认为根据你的作品有不同的立场.我倾向于生成LOB应用程序,这些应用程序需要在内部与其他应用程序和团队进行互操作,因此任何给定应用程序的公共API都有一些利益相关者.但是,这并不像为许多未知客户端生成库或框架那样极端,因为公共API突然变得更加重要.

在部署方案中,如果您更改了实现,理论上您可以部署该单个DLL - 例如,仅留下UI和接口DLL.如果您一起编译了接口和实现,则可能需要重新部署UI DLL ...

另一个好处是清理隔离你的代码 - 有一个接口(或共享库)DLL明确说明开发团队的任何地方放置新类型等.我不再把它作为一个好处,因为我们还没有如果没有这样做的任何问题,无论接口放在何处,公共合同仍然很容易找到.

我不知道是否存在最佳实践或者反对,重要的是,在代码中,您总是在使用接口,并且永远不会让任何代码泄漏到使用实现.


Dav*_*001 14

到目前为止,答案似乎都说将接口放在自己的程序集中是"通常"的做法.我不同意将不相关的接口放入一个"共享"公共程序集中,因此这意味着我需要为每个"实现"程序集设置一个接口程序集.

然而,进一步考虑它,我想不出这个实践的许多真实世界的例子(例如,log4netNUnit提供公共接口程序集,以便消费者可以决定不同的实现吗?如果是这样,nunit的其他实现可以我用?).通过谷歌花费年龄,我发现了一些资源.

  • 在证明你的观点时,我认为你没有完全代表其他答案,你已经接受了自己的答案.BTW log4net和NUnit不是应用程序,它们是库.我不认为这太微妙了. (7认同)
  • 我知道这是旧的,但我想指出System.Data遵循类似的模式,主要是它定义了与数据库交谈的接口,如IDbConnection,IDbCommand等......而数据库特定的程序集定义了具体的实现. (3认同)
  • 他们绝对应该在自己的集会中,这就是证据。DLL 具有不能包含循环的逻辑依赖关系图。因此,您将拥有较低级别的 DLL,例如用于 DTO 的 DLL,这些 DLL 可以由更高级别的程序集(例如业务逻辑程序集)引用。如果您将 ISystemLogger 接口放在 BLL 层中,您将无法通过在较低层中的依赖注入使用它来记录系统错误(例如在 Dispose 方法中,该方法不应引发异常),除非您引用业务逻辑 DLL,这可能是不可能的。 (3认同)
  • 因此,为了利用依赖注入,接口需要自己访问,或者至少在依赖图中的最低点。没有这样的限制应该适用于实际的实现,因此,有一个非常强大的例子让它们在一个独立于它们的实现的程序集中。此外,将它们放在单独的程序集中确实可以更轻松地换出实现,因为您只需要换出实现 DLL。 (2认同)

Tim*_*oyd 9

我所遵循的模式我称之为共享类型(我也使用DI)是一个单独的程序集,其中包含以下应用程序级概念(而不是常见程序集中的常见概念):

  1. 共享接口.
  2. DTO的.
  3. 例外.

通过这种方式,可以管理客户端和核心应用程序库之间的依赖关系,因为客户端不能直接依赖于具体实现,也不能作为添加直接程序集引用然后访问任何旧公共类型的意外后果.

然后我有一个运行时类型设计,我在应用程序启动时设置我的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
  --...
Run Code Online (Sandbox Code Playgroud)


Luk*_*ett 6

我同意勾选答案.大卫,对你有好处.事实上,我很高兴看到答案,以为我疯了.

在企业C#自由职业者工作中,我总是看到这个有趣的"钢笔盆"模式,人们遵循人群的惯例,团队必须遵守,不符合就是制造麻烦.

另一个疯狂的是每个程序集废话的一个命名空间.所以你得到一个SomeBank.SomeApp.Interfaces命名空间,一切都在其中.

对我来说,这意味着类型分散在命名空间和程序集中,包含一大堆我不关心的东西必须在所有地方被引用.

至于接口,我甚至不在我的私人应用程序中使用接口; DI适用于类型,具体的虚拟,基类或接口.我相应地选择并根据它们的作用在DLL中放置类型.

我以后从未遇到过DI或交换逻辑的问题.

•.NET程序集是安全性,API范围和部署的一个单元,与命名空间无关.

•如果两个程序集相互依赖,则它们无法单独部署和版本化,应合并.

•拥有许多DLL通常意味着将大量内容公开,因此很难从必须公开的类型成员中分辨出实际的公共API,因为它们被任意放入自己的程序集中.

•我的DLL之外的代码是否需要使用我的类型?

•开始保守; 我通常可以轻松地将一个类型移出一个层,而另一方面则更难.

•我是否可以将我的功能区域或框架整齐地打包到NuGet包中,使其完全可选且可版本化,就像任何其他包一样?

•我的类型是否与特征的交付一致,是否可以放在特征命名空间中?

•许多真正的库和框架都被标记,使它们易于讨论,并且它们不会烧毁暗示其使用或含糊不清的命名空间名称,我是否可以使用像Steelcore这样的"代码名称"而不是泛型来标记我的应用程序的组件陈词滥调和令人困惑的条款,错误的'服务'?

编辑

这是我今天在开发中看到的一个被误解的事情.太糟糕了.

您有一个API,因此将其所有类型放在单个API项目中.只有在需要共享/重用它们时才将它们移出.当您将它们移出时,将它们直接移动到NuGet包中,该包具有带有包的意图和焦点的清晰名称.如果你正在努力争取一个名字,并考虑"共同",那可能是因为你正在创造一个倾销场.

您应该将NuGet包计入一系列相关包.您的"核心"包应该对其他包具有最小的依赖性.内部类型与使用相关并且相互依赖.

然后,为需要其他依赖项集的更专业的类型和子类型创建一个新包.更清楚:您通过外部依赖关系拆分库,而不是通过类型或其接口或异常.

图书馆保理

因此,您可能会将所有类型都放在一个大型库中,但是一些更专业的类型(彩色点)依赖于某些外部库,所以现在您的库需要引入所有这些依赖项.这是不必要的,您应该将这些类型分解为更多专门的库,这些库确实需要所需的依赖项.

包A和B中的类型可以属于同一个命名空间.引用A引入一组类型,然后可选地引用B补充命名空间.

而已.

卢克