【问题标题】:Architecture: Dependency Injection, Loosely Coupled Assemblies, Implementation Hiding架构:依赖注入、松散耦合的程序集、实现隐藏
【发布时间】:2017-05-28 22:08:56
【问题描述】:

我一直在从事一个个人项目,除了制作对自己有用的东西之外,我还尝试将其用作继续寻找和学习建筑课程的一种方式。一个这样的教训就像在自行车道中间出现的科迪亚克熊一样,我一直在努力解决它。

这个问题本质上是依赖注入、程序集解耦和实现隐藏(即使用内部类实现我的公共接口)交叉点的问题的混合体。

在我的工作中,我通常发现应用程序的各个层都有自己的接口,它们公开公开,但在内部实现。每个程序集的 DI 代码将内部类注册到公共接口。此技术可防止外部程序集更新实现类的实例。但是,我在构建此解决方案时一直在阅读的一些书籍对此表示反对。与我之前的想法冲突的主要事情与 DI 组合根有关,以及应该在哪里保留给定实现的接口。如果我将依赖注册移动到单个全局组合根(正如 Mark Seemann 建议的那样),那么我可以摆脱每个程序集必须运行自己的依赖注册。然而,缺点是实现类必须是公共的(允许任何程序集实例化它们)。至于解耦程序集,Martin Fowler 指示将接口与使用接口的代码一起放入项目中,而不是实现它的代码。例如,这是他提供的图表,相比之下,我通常如何实现相同的解决方案的图表(好吧,这些并不完全相同;请关注箭头并注意实现箭头何时交叉装配边界而不是合成箭头)。

马丁风格

我平时看到的

我立即在 Martin 的图表中看到了优势,它允许将较低的程序集换成另一个程序集,因为它有一个在其上层实现接口的类。然而,我也看到了这个看似主要的缺点:如果你想从上层换出程序集,你实际上是在“偷走”下层正在实现的接口。

在考虑了一会儿之后,我决定在两个方向上完全解耦的最佳方法是在它们自己的程序集中使用指定层之间契约的接口。考虑这个更新的图表:

这很疯狂吗?开对了吗?对我来说,这似乎解决了接口隔离的问题。但是,它并没有解决无法将实现类隐藏为内部的问题。那里有什么合理的可以做的吗?我不应该担心这个吗?

我脑子里想的一个解决方案是让每一层实现代理层的接口两次;一次是公共类,一次是内部类。这样,公共类就可以只包装/装饰内部类,如下所示:

某些代码可能如下所示:

namespace MechanismProxy // Simulates Mechanism Proxy Assembly
{
    public interface IMechanism
    {
        void DoStuff();
    }
}

namespace MechanismImpl // Simulates Mechanism Assembly
{
    using MechanismProxy;

    // This class would be registered to IMechanism in the DI container
    public class Mechanism : IMechanism
    {
        private readonly IMechanism _internalMechanism = new InternalMechanism();

        public void DoStuff()
        {
            _internalMechanism.DoStuff();
        }
    }

    internal class InternalMechanism : IMechanism
    {
        public void DoStuff()
        {
            // Do whatever
        }
    }
}

...当然,我仍然需要解决一些关于构造函数注入和将注入公共类的依赖项传递给内部类的问题。还有一个问题是外部程序集可能会更新公共机制......我需要一种方法来确保只有 DI 容器才能做到这一点......我想如果我能弄清楚,我什至不需要内部版本。无论如何,如果有人能帮助我了解如何克服这些架构问题,将不胜感激。

【问题讨论】:

  • 解决这个问题的一种方法是让所有层都知道 DI 抽象。他们每个人都会公开他们的接口。如果您不希望其他程序集能够更新您的实现,它们会将它们保留在内部并通过 DI 抽象将它们添加到组合根。内核内置的 DI 就是一个很好的例子。
  • 对我来说你太偏执了;o)
  • 当您开始使用 AutoFac 或 Ninject 等实际 DI 实现时,这种完全不可知的 DI 的想法就会崩溃……它们都有一些独特的东西,如果您不让他们知道什么是 DI你正在使用一些基本的东西,在很多情况下都不起作用。如果您曾经尝试从 SQL db 切换到 MongoDB,这与存储库模式相同,它不会像宣传的那么简单。
  • @bubbleking 看看这篇介绍性文章msdn.microsoft.com/en-us/magazine/mt703433.aspx
  • @bubbleking 我并不是真的建议放弃 DI,只是说没有一种正确的做事方式。这取决于你在做什么。例如,如果扭曲依赖于框架的东西,那么 DI 参考是更好的做事方式(类似于 prism 模块的东西)。如果您正在扭动 Nuget,您不希望依赖于其他事情,让用户决定何时使用 new 是有用的(他们可能正在使用一些奇怪的功能架构以及为什么强制 DI)。如果您不希望他们知道实现,则可以使用静态工厂方法。

标签: c# .net design-patterns dependency-injection architecture


【解决方案1】:

但是,缺点是实现类必须是公共的(允许任何程序集实例化它们)。

除非您正在构建一个可重用的库(在 NuGet 上发布并被您无法控制的其他代码库使用),否则通常没有理由将类设为内部。特别是因为您对接口进行编程,应用程序中唯一依赖于这些类的地方就是组合根。

另外请注意,如果您将抽象移动到不同的库,并让使用程序集和实现程序集都依赖于该程序集,则这些程序集不必相互依赖。这意味着这些类是公共的还是内部的根本不重要。

然而,这种级别的分离(将接口放置在自己的程序集中)几乎不需要。最后,这完全取决于部署期间所需的粒度和应用程序的大小。

至于解耦程序集,Martin Fowler 指示将接口与使用接口的代码一起放入项目中,而不是实现它的代码。

这是Dependency Inversion Principle,上面写着:

在依赖倒置的直接应用中,抽象归上层/策略层所有

【讨论】:

    【解决方案2】:

    这是一个有点基于意见的话题,但既然你问了,我就给我。

    您专注于创建尽可能多的组件以使其尽可能灵活是非常理论化的,您必须权衡实际价值与成本。 不要忘记程序集只是编译代码的容器。只有当您查看开发、构建和部署/交付它们的过程时,它们才变得最重要。因此,您必须提出更多问题才能对如何准确地将代码拆分为程序集做出正确的决定。

    所以这里有几个我事先要问的问题示例:

    从您的应用程序中拆分 以这种方式组装(例如,你真的需要换掉吗? 程序集)?

    您是否有单独的团队来开发这些?

    范围的规模(LOC 和团队规模)是多少?

    是否需要保护实现不可用/不可见?例如。是外部接口还是内部接口?

    您真的需要将程序集作为一种机制来强制执行架构分离吗?还是有其他更好的措施(例如代码审查、代码检查器等)?

    您的调用真的只发生在程序集之间,还是在某些时候需要远程调用?

    您必须使用私有程序集吗?

    密封类是否有助于强化您的架构?

    对于一个非常笼统的观点,将这些额外的因素排除在外,我会支持 Martin Fowler 的图表,因为这只是如何提供/使用接口的标准方式。如果您对问题的回答表明通过进一步拆分/保护可能没问题的代码具有附加价值。但是您必须告诉我们更多有关您的应用程序域的信息,并且您必须能够很好地证明它的合理性。

    因此,在某种程度上,您面临着两种古老的智慧:

    1. 架构倾向于遵循组织设置。
    2. 过度设计(过度复杂)架构很容易,但很难将它们设计得尽可能简单。大多数情况下,简单会更好。

    在提出架构时,您需要预先考虑这些因素,否则它们会在以后以技术债务的形式困扰您。

    【讨论】:

    • 我同意这一点:程序集是部署工件。在不同程序集中拆分代码的主要原因是能够单独部署它们,如here 所述。
    • @Steven - 毫不奇怪,这个 SO 问题一直是我在处理此问题时一遍又一遍地提到的问题。
    【解决方案3】:

    但是,缺点是实现类必须是公共的(允许任何程序集实例化它们)。

    这听起来不像是缺点。由于某些其他原因,绑定到您的组合根中的抽象的实现类可能会在其他地方以显式方式使用。我看不出隐藏它们有什么好处。

    我需要一种方法来确保只有 DI 容器才能做到这一点……

    不,你没有。

    您的困惑可能源于您认为 DI 和 Composition Root 就像后面肯定有一个容器一样。

    然而,事实上,从某种意义上说,基础架构可以完全“与容器无关”,即您仍然需要注入依赖项,但您不会考虑“如何”。使用容器的组合根是您的选择,最好选择另一个组合根,您可以在其中手动组合依赖项。换句话说,Composition Root 可能是代码中唯一知道 DI 容器(如果使用的话)的地方。您的代码是针对依赖倒置的想法构建的,而不是依赖倒置容器的想法。

    我的一个简短教程可能会在这里有所启发

    http://www.wiktorzychla.com/2016/01/di-factories-and-composition-root.html

    【讨论】:

    • 您的链接文章很有帮助。
    猜你喜欢
    • 2013-05-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-03-15
    • 1970-01-01
    • 1970-01-01
    • 2012-02-27
    • 1970-01-01
    相关资源
    最近更新 更多