我一直致力于个人项目,除了为自己创造有用的东西之外,我还试图用它来继续寻找和学习建筑课程。一个这样的教训就像一辆科迪亚克熊在自行车道中间出现,而且我一直在努力奋斗。
问题本质上是依赖注入,程序集解耦和实现隐藏(即使用内部类实现我的公共接口)的交叉问题的混合。
在我的工作中,我通常发现应用程序的各个层都有自己的接口,这些接口是公开公开的,但在内部实现。每个程序集的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容器可以做到这一点...我想如果我能弄明白,我就不会甚至需要内部版本。无论如何,如果有人能帮助我理解如何克服这些建筑问题,那将非常感激。
答案 0 :(得分:3)
但是,缺点是实现类必须是公共的(允许任何程序集实例化它们)。
除非您正在构建一个可重用的库(它在NuGet上发布并被其他代码库使用而无法控制),否则通常没有理由将类作为内部。特别是因为你编程接口,应用程序中依赖于这些类的唯一位置是组合根。
另请注意,如果您将抽象移动到不同的库,并且让消耗和实现程序集都依赖于该程序集,那些程序集就不必相互依赖。这意味着这些课程是公开的还是内部的并不重要。
然而,几乎不需要这种分离级别(将接口放置在它自己的组件中)。最后,它是关于部署期间所需的粒度和应用程序的大小。
对于解耦组件,Martin Fowler指示使用接口的代码将接口放在项目中,而不是实现接口的接口。
这是Dependency Inversion Principle,其中声明:
在直接应用依赖项反转时,摘要由上层/政策层拥有
答案 1 :(得分:2)
这是一个基于意见的主题,但既然你问过,我会给我的。
您专注于尽可能多地创建尽可能多的装配是理论上的,您必须权衡实际价值与成本。 不要忘记程序集只是编译代码的容器。只有当您查看开发,构建和部署/交付流程时,它们才变得最相关。因此,在准确地将代码拆分为程序集之前,您必须先提出更多问题。
所以这里有几个我事先要问过的问题的例子:
从您的应用域分割出来是否有意义 以这种方式组装(例如,你真的需要换掉 组件)?
您是否会有独立的团队来开发这些团队?
范围在大小(LOC和团队规模)方面有什么作用?
是否需要保护实施不可用/可见?例如。那些外部接口还是内部接口?
您真的需要依靠程序集作为强制架构分离的机制吗?还是有其他更好的措施(例如代码审查,代码检查等)?
您的呼叫是否真的只发生在程序集之间,还是在某些时候需要远程呼叫?
你必须使用私人集会吗?
密封课会帮助改进您的架构吗?
对于非常普遍的看法,将这些额外的因素排除在外,我会支持Martin Fowler的图表,因为这只是提供/使用接口的标准方法。如果您对问题的回答通过进一步分割/保护可能没问题的代码来表明其他价值。但是你必须告诉我们更多关于你的应用领域的内容,你必须能够证明它的合理性。
所以在某种程度上,你面临着两个古老的智慧:
在提出建筑时,你想先考虑这些因素,否则它们会以技术债务的形式出现在你身上。
答案 2 :(得分:2)
但是,缺点是实现类必须是公共的(允许任何程序集实例化它们)。
这听起来不像是一个缺点。由于某些其他原因,绑定到Composition Root中的抽象的实现类可能会以其他方式以明确的方式使用。我认为隐藏它们没有任何好处。
我需要一种方法来确保只有DI容器可以做到这一点......
不,你没有。
你的困惑可能源于这样一个事实,你肯定会想到DI和组合根,就像背后有一个容器一样。
然而,事实上,基础设施可能完全是“与容器无关”的,因为您仍然注入了依赖关系,但您没有想到“如何”。使用容器的Composition Root是您的选择,作为您可以手动编写依赖关系的另一个Composition Root的最佳选择。换句话说,如果使用了任何DI容器,则组合根可能是代码中唯一知道DI容器的位置。你的代码是依赖于依赖倒置的想法而不是依赖倒置容器的想法。
我的简短教程可能会在这里阐明一些亮点
http://www.wiktorzychla.com/2016/01/di-factories-and-composition-root.html