考虑到具有n层体系结构和依赖项注入的中型软件,我很高兴地说属于一个层的对象可以依赖于较低层的对象,而不能依赖于较高层的对象。
但是我不确定要考虑那些依赖同一层其他对象的对象。
举个例子,我们假设一个应用程序具有三层和几个对象,如图像中的一个。显然,自上而下的依赖关系(绿色箭头)没问题,自下而上的依赖关系(红色箭头)不行,但是同一层内的依赖关系(黄色箭头)又如何呢?
除了循环依赖之外,我很好奇其他可能出现的问题以及这种情况下违反了多层体系结构的程度。
考虑到具有n层体系结构和依赖项注入的中型软件,我很高兴地说属于一个层的对象可以依赖于较低层的对象,而不能依赖于较高层的对象。
但是我不确定要考虑那些依赖同一层其他对象的对象。
举个例子,我们假设一个应用程序具有三层和几个对象,如图像中的一个。显然,自上而下的依赖关系(绿色箭头)没问题,自下而上的依赖关系(红色箭头)不行,但是同一层内的依赖关系(黄色箭头)又如何呢?
除了循环依赖之外,我很好奇其他可能出现的问题以及这种情况下违反了多层体系结构的程度。
Answers:
是的,一层中的对象之间可以具有直接依赖关系,有时甚至具有周期性的依赖关系,这实际上是不同层中对象之间允许的依赖关系的核心区别,在这种情况下,不允许直接依赖关系,或者只是严格的依赖关系方向。
但是,这并不意味着它们应该以任意方式具有这种依赖性。实际上,这取决于您的层代表什么,系统有多大以及部件的职责是什么。请注意,“分层架构”是一个模糊的术语,在不同类型的系统中实际含义有很大的不同。
例如,假设您有一个“水平分层系统”,其中包含数据库层,业务层和用户界面(UI)层。让我们说,UI层至少包含几十个不同的对话框类。
可以选择一种设计,其中所有对话框类都不直接依赖于另一个对话框类。可以选择一种设计,其中存在“主对话框”和“子对话框”,并且只有从“主”对话框到“子”对话框的直接依赖项。或者可能更喜欢一种设计,其中任何现有的UI类都可以使用/重用同一层中的任何其他UI类。
这些都是可能的设计选择,或多或少是明智的选择,具体取决于要构建的系统的类型,但是没有一个选择会使系统的“分层”无效。
我很高兴地说,属于某个层的对象可以依赖于较低层的对象
老实说,我认为您不应该对此感到满意。当处理一个琐碎的系统时,我的目标是确保所有层仅依赖于来自其他层的抽象。越来越低。
因此,例如,Obj 1
不应依赖Obj 3
。它应该依赖于例如,IObj 3
并且应该被告知在运行时将使用哪种抽象实现。进行告知的事情应该与任何级别无关,因为它的工作是映射那些依赖关系。那可能是一个IoC容器,例如main
使用纯DI 调用的自定义代码。或者一推再推,它甚至可能是服务定位器。无论如何,在该层提供映射之前,层之间不存在依赖项。
但是我不确定要考虑那些依赖同一层其他对象的对象。
我认为这是您唯一应该具有直接依赖关系的时间。它是该层内部工作的一部分,可以更改而不会影响其他层。因此,这不是有害的耦合。
让我们看看这个
Obj 3
现在知道Obj 4
存在。所以呢?我们为什么在乎?
DIP说
“高级模块不应依赖于低级模块。两者都应依赖于抽象。”
好,但是,不是所有的对象抽象吗?
DIP还说
“抽象不应该依赖细节。细节应该依赖抽象。”
好的,但是,如果我的对象正确封装了,那不会隐藏任何细节吗?
有些人喜欢盲目地坚持每个对象都需要一个关键字接口。我不是其中之一。我确实想盲目地坚持认为,如果您现在不打算使用它们,则需要一个计划来应对以后需要它们的东西。
如果您的代码在每个发行版上都可以完全重构,则可以在以后需要时提取接口。如果您已经发布了不想重新编译的代码,并且发现自己希望通过接口进行交谈,则需要一个计划。
Obj 3
知道Obj 4
存在。但是不Obj 3
知道Obj 4
具体吗?
这就是为什么不分散到new
任何地方如此好。如果Obj 3
不知道是否Obj 4
具体,很可能是因为它没有创建它,那么如果您稍后Obj 4
插入并变成一个抽象类Obj 3
,则不在乎。
如果可以做到的话,那么Obj 4
一直都是完全抽象的。从一开始就在他们之间建立接口的唯一方法就是确保某人不会不小心添加现在给出的Obj 4
具体代码。受保护的构造函数可以减轻这种风险,但这会导致另一个问题:
Obj 3和Obj 4是否在同一包装中?
对象通常以某种方式(包,名称空间等)进行分组。当明智地分组时,变化在组内而不是跨组的可能性更大。
我喜欢按功能分组。如果Obj 3
和Obj 4
在同一组和同一层中,则您极不可能发布一个并且不想重构,而只需要更改另一个。这意味着在有明确需求之前,在它们之间放上抽象就不太可能从中受益。
如果您越过组边界,最好让任一侧的对象独立变化。
应当很简单,但不幸的是,Java和C#都做出了不幸的选择,使选择复杂化。
在C#中,传统的做法是用I
前缀为每个关键字接口命名。这迫使客户知道他们正在与关键字界面进行对话。这与重构计划不符。
在Java中,通常使用更好的命名模式:FooImple implements Foo
但是,这仅在源代码级别有用,因为Java会将关键字接口编译为其他二进制文件。这意味着当您Foo
从具体重构到抽象客户端时,不需要更改代码的单个字符,仍然必须重新编译。
正是这些特定语言中的错误使人们无法推迟正式的抽象,直到他们真正需要它为止。您没有说使用什么语言,但了解有些语言根本没有这些问题。
您没有说您使用的是哪种语言,所以我只是敦促您在决定它将成为所有关键字界面之前,仔细分析您的语言和情况。
YAGNI原则在这里起着关键作用。但是“请加倍努力地朝自己的脚射击”也是如此。
除了上面的答案,我认为它可能会帮助您从不同的角度来看它。
例如,从“ 依赖关系规则”的角度来看。罗伯特C.马丁(Robert C. Martin)提出了著名的“ 清洁建筑”规则。
它说
源代码依赖性必须仅指向内部,指向更高级别的策略。
通过更高级别的策略,他意味着更高级别的抽象。在实现细节上泄漏的组件,例如与具体类或数据结构形成对比的接口或抽象类。
问题是,规则不限于层间依赖性。它仅指出代码段之间的依赖关系,而不管它们属于什么位置或层。
所以不,在同一层的元素之间具有依赖关系并不是天生的错误。但是,依存关系仍然可以用稳定的依存关系原则来传达。
另一个观点是SRP。
去耦是我们打破有害依赖关系并传达一些最佳实践(如依赖关系倒置(IoC))的方式。但是,那些具有更改原因的元素不会给出解耦的原因,因为具有相同更改原因的元素将同时(非常有可能)更改,并且它们也将同时部署。如果是两次之间的情况Obj3
,Obj4
那么再也没有天生的错误。