对于某些开发人员而言,松散耦合是精心设计的软件的圣杯。当它使代码在可预见的将来可能发生的变化面前更加灵活,或者避免代码重复时,这无疑是一件好事。
另一方面,松散耦合组件的努力增加了程序中的间接访问量,从而增加了程序的复杂性,常常使程序难以理解,并常常使效率降低。
您是否认为在没有任何松散耦合用例的情况下专注于松散耦合(例如避免代码重复或计划在可预见的将来可能发生的更改)是一种反模式?松散的联轴器能否落入YAGNI的保护伞下?
对于某些开发人员而言,松散耦合是精心设计的软件的圣杯。当它使代码在可预见的将来可能发生的变化面前更加灵活,或者避免代码重复时,这无疑是一件好事。
另一方面,松散耦合组件的努力增加了程序中的间接访问量,从而增加了程序的复杂性,常常使程序难以理解,并常常使效率降低。
您是否认为在没有任何松散耦合用例的情况下专注于松散耦合(例如避免代码重复或计划在可预见的将来可能发生的更改)是一种反模式?松散的联轴器能否落入YAGNI的保护伞下?
Answers:
编程实践 X是好是坏? 显然,答案总是 “取决于情况”。
如果您正在查看代码,想知道可以注入哪些“模式”,那么您做错了。
如果您正在构建软件,以使不相关的对象不会相互纠缠,那么您做对了。
如果您正在“设计”您的解决方案以使其可以无限扩展和更改,那么您实际上会使它变得更加复杂。
我认为到最后,您只剩下一个道理:分离对象是否更复杂?如果将它们耦合起来不太复杂,那么这就是正确的解决方案。如果解耦它们不那么复杂,那么这就是正确的解决方案。
(我目前正在一个相当小的代码库中工作,它以非常复杂的方式完成一项简单的工作,而使之如此复杂的部分原因是,原始人对术语“耦合”和“凝聚力”缺乏理解开发人员。)
我认为您在这里得到的是凝聚力的概念。此代码是否有良好的用途?我可以内化这个目的,并了解正在发生的事情的“全局”吗?
这可能会导致难以理解的代码,不仅是因为源文件更多(假设它们是单独的类),而且还因为似乎没有单个类具有目的。
从敏捷的角度来看,我可能会建议这种松散的耦合将是一种反模式。没有凝聚力,甚至没有用例,您就无法编写明智的单元测试,也无法验证代码的用途。现在,敏捷代码可能导致松耦合,例如,在使用测试驱动的开发时。但是,如果以正确的顺序创建了正确的测试,则可能同时存在良好的内聚性和松散的耦合性。您所谈论的只是明显没有凝聚力的情况。
同样,从敏捷的角度来看,您不希望这种人为的间接级别,因为这浪费了精力,可能根本不需要使用某些东西。当真正需要时,重构就容易得多。
总体而言,您希望模块之间保持高耦合,而模块之间则需要松散耦合。没有高耦合,您可能没有凝聚力。
对于大多数这样的问题,答案是“取决于情况”。总的来说,如果我能够以合理的方式使设计在逻辑上松散耦合,而又没有太多的开销,那么我会。在我看来,避免代码中不必要的耦合是一个完全值得的设计目标。
一旦出现逻辑上似乎应该将组件紧密耦合的情况,在开始分解它们之前,我将寻找一个令人信服的论点。
我猜想我在大多数这类实践中遵循的原则是惯性之一。我对我的代码工作方式有一个想法,如果我能做到这一点而又不让生活变得更艰难,那我会的。如果这样做会使开发难度加大,但使维护和将来的工作变得容易,那么我将尝试推测在整个代码生命周期中是否还会进行更多工作,并以此为指导。否则,这将是一个值得考虑的故意设计点。
一个简单的答案是正确完成松耦合是好的。
如果遵循一种功能的原则,那么遵循一个目的,那么遵循发生的事情应该足够容易。同样,松散的夫妇代码当然也无需费力。
简单的设计规则:1.除非要构建外观界面,否则不要将多个项目的知识整合到一个点中(如各地所指出的那样,取决于情况)。2.一个功能-一个目的(该目的可能是多方面的,例如在立面中)3.一个模块-一组清晰的相互关联的功能-一个明确的目的4.如果不能简单地进行单元测试,那么它就没有一个简单的目的
所有这些关于以后更容易重构的评论都是鳕鱼的负担。一旦知识积累到许多地方,尤其是在分布式系统中,重构的成本,部署同步以及几乎所有其他成本就将其浪费了很多,以至于在大多数情况下,系统最终都会因此而浪费。
如今,有关软件开发的可悲的事情是,有90%的人开发新系统并且没有能力理解旧系统,并且由于系统不断地进行点点滴滴的重构而使系统处于如此糟糕的状态时,他们永远都不会消失。
如果一件事物永不改变,那么一件事物与另一事物之间的紧密关联并不重要。多年来,我发现通常集中精力于寻找更少的事物发生变化的原因,寻求稳定性,而不是通过尝试实现最宽松的耦合形式而使它们更容易变化,这更具生产力。 解耦我发现非常有用,以至于有时我倾向于使用适度的代码复制来解耦程序包。作为一个基本示例,我可以选择使用数学库来实现图像库。我没有,只是复制了一些基本的数学函数,这些函数很容易复制。
现在,我的图像库在某种程度上完全独立于数学库,无论我对数学库进行何种更改,都不会影响图像库。这将稳定性放在首位。图像库现在更稳定了,因为更改的理由大大减少了,因为它与可能更改的任何其他库(除了C标准库(希望永远都不会更改))脱钩。另外,当它只是一个独立的库,不需要引入大量其他库来构建和使用它时,它也易于部署。
稳定性对我很有帮助。我喜欢构建一组经过良好测试的代码,这些代码将来有越来越少的更改理由。那不是白日梦。自80年代末以来,我一直在使用C语言代码,并且一直在使用,从那时起就一直没有改变。诚然,它是低级的东西,例如面向像素和与几何相关的代码,而我的许多高级东西已经过时了,但这仍然有很多帮助。这几乎总是意味着一个依赖于越来越少事物的库,即使根本没有外部依赖。如果您的软件越来越依赖稳定的基础,而这些基础很少或根本没有理由进行更改,那么可靠性就会越来越高。更少的运动部件确实很棒,即使实际上运动部件的数量比稳定部件大得多。
松散耦合具有相同的脉络,但我经常发现,松散耦合比没有耦合的稳定性差很多。除非您与非常出色的界面设计师和客户一起工作,并且他们的主意不会像以前那样改变主意,否则即使是纯界面也常常会找到改变方式的原因,这些方式仍然会导致整个代码的级联损坏。通过将依赖关系指向抽象而不是具体对象可以实现稳定性的想法只有在接口设计比实现更容易在第一时间就正确时才有用。在开发人员可能给他们以为他们应该满足的设计要求的情况下,他们可能创建了一个很好的,甚至不是很好的实现的情况下,我常常发现这种情况发生了逆转,但后来发现设计要求完全改变了。
因此,我喜欢稳定性和完全去耦,因此我至少可以自信地说:“这个混乱的小库已经使用了多年,并且经过全面的测试,几乎没有可能进行更改,无论外界环境如何”。无论需要外部进行何种设计更改,这都给我带来了一点理智。
耦合和稳定性,ECS示例
我也喜欢实体组件系统,它们引入了很多紧密的耦合,因为系统对组件的依赖关系都直接访问和操纵原始数据,如下所示:
由于组件仅公开原始数据,因此这里的所有依赖项都非常严格。依赖关系并没有流向抽象,而是流向了原始数据,这意味着每个系统对它们请求访问的每种类型的组件都拥有最大的知识量。组件没有功能,所有系统都必须访问和篡改原始数据。但是,由于这样的系统非常扁平,因此很容易推断出这样的系统。如果纹理出问题了,那么您会立即通过该系统知道只有渲染和绘画系统才能访问纹理组件,并且由于它仅从概念上读取纹理,因此您可以快速排除渲染系统。
同时,一个松散耦合的选择可能是:
...所有的依赖关系都流向抽象函数,而不是数据,并且该图中的每件事都公开了自己的公共接口和功能。这里所有的依赖关系可能都非常松散。这些对象甚至可能不会直接相互依赖,也不会通过纯接口相互交互。仍然很难对这个系统进行推理,特别是考虑到交互作用的复杂性,如果出现问题,则尤其如此。与ECS相比,还将有更多的交互(更多的耦合,尽管更宽松),因为实体必须知道它们聚合的组件,即使它们只知道彼此的抽象公共接口。
同样,如果对任何事物进行了设计更改,则与ECS相比,您会遭受更多的级联破坏,并且通常会有更多的原因和诱惑来进行设计更改,因为每件事都试图提供一个不错的面向对象的界面和抽象。随之而来的想法是,每件小事都会试图对设计施加约束和限制,而这些约束通常是保证设计变更的原因。功能比原始数据受更多的约束,并且必须做出更多的设计假设。
在实践中,我发现上述类型的“扁平式” ECS系统甚至比具有松散依赖关系的复杂蜘蛛网的最松散耦合的系统要容易得多,而且对我而言最重要的是,我发现的原因很少对于ECS版本,由于需要依赖的组件除了提供系统运行所需的适当数据外,没有其他责任,因此他们需要更改任何现有组件。比较设计纯IMotion
接口和实现该接口的具体运动对象的困难,该对象提供复杂的功能,同时尝试保持私有数据不变,而运动组件仅需要提供相关的原始数据来解决问题,而不会麻烦功能。
功能远比数据难实现,这就是为什么我认为将依赖关系流直接指向数据通常更可取。毕竟,那里有多少个向量/矩阵库?他们中有多少人使用完全相同的数据表示形式,但在功能上仅略有不同?无数,尽管数据表示相同,但我们仍然有很多,因为我们想要功能上的细微差别。那里有多少个图像库?其中有多少以不同且独特的方式表示像素?几乎没有,并且再一次表明,在许多情况下,功能比数据更不稳定并且更容易进行设计更改。当然在某些时候我们需要功能,但是您可以设计将大部分依赖关系流向数据的系统,而不是一般的抽象或功能。这将优先考虑耦合之上的稳定性。
我曾经编写过的最稳定的函数(自80年代末以来我一直在使用和重用的函数,根本不需要更改它们)都是依赖于原始数据的函数,例如几何函数,它只接受一个数组浮点数和整数,而不是那些依赖复杂的上Mesh
对象或IMesh
接口,或向量/矩阵乘法刚刚依赖于float[]
或double[]
,不是一个依赖FancyMatrixObjectWhichWillRequireDesignChangesNextYearAndDeprecateWhatWeUse
。