面向对象设计中的松耦合


16

我正在尝试学习GRASP,我发现了有关低耦合的解释(在第3页上),当我发现这一点时,我感到非常惊讶:

考虑addTrack一个Album类的方法,两种可能的方法是:

addTrack( Track t )

addTrack( int no, String title, double duration )

哪种方法可以减少耦合?第二个则需要,因为使用Album类的类不必知道Track类。通常,方法的参数应使用基本类型(int,char ...)和java。*包中的类。

我倾向于不同意这一点。我相信addTrack(Track t)胜于addTrack(int no, String title, double duration)各种原因:

  1. 最好总是使用尽可能少的参数的方法(根据Bob叔叔的“清洁代码”,最好是无或一个,在某些情况下为2,在特殊情况下为3;超过3的需求需要重构-这些当然是建议,而不是冬青规则) 。

  2. 如果addTrack是接口的一种方法,并且要求a Track应该具有更多的信息(例如年份或类型),则需要更改接口,以便该方法应支持另一个参数。

  3. 封装破裂;如果addTrack在接口中,则它不应该知道的内部Track

  4. 实际上,它在第二种方式中与许多参数耦合在一起。假设no参数需要被改变,从intlong,因为有超过MAX_INT轨道(或无论何种原因); 则Track必须同时更改和方法,而如果addTrack(Track track)仅更改方法,则需要Track更改。

这四个参数实际上是相互关联的,其中一些是其他因素的结果。

哪种方法更好?


2
这是教授或培训人员整理的文件吗?根据您提供的链接的URL,它看起来像是一个类,尽管我看不到文档中有关创建它的人的任何信息。如果这是课堂的一部分,我建议您向提供文档的人询问这些问题。顺便说一句,我确实同意您的推理-在我看来,专辑类可能想要固有地了解Track类。
Derek

老实说,每当我读到“最佳实践”时,我都会带着一粒盐!
AraK 2013年

@Derek我通过在Google上搜索“抓取模式示例”找到了该文档;我不是谁写的,但由于它是一所大学的,所以我相信它是可靠的。我正在寻找一个基于给定信息的示例,而忽略了来源。
m3th0dman 2013年

4
@ m3th0dman“但是由于它来自一所大学,我相信它是可靠的。” 对我来说,因为它来自一所大学,所以我认为它是不可靠的。我不相信没有从事多年项目的人谈论软件开发的最佳实践。
AraK

1
@AraK可靠并不意味着毫无疑问;这就是我在这里所提出的问题。
m3th0dman 2013年

Answers:


15

好吧,您的前三点实际上是关于耦合以外的其他原理的。您总是必须在经常冲突的设计原则之间取得平衡。

您的第四点关于耦合的,我非常同意。耦合是关于模块之间的数据流。数据流入的容器的类型在很大程度上不重要。将持续时间以两倍而不是a的Track形式传递并不能消除传递时间的需要。这些模块仍然需要共享相同数量的数据,并且仍然具有相同数量的耦合。

他还没有将系统中的所有耦合视为一个整体。引入Track类固然会在两个单独的模块之间添加另一个依赖关系,但它可以显着减少系统的耦合,这是此处的重要措施。

例如,考虑一个“添加到播放列表”按钮和一个Playlist对象。Track如果仅考虑这两个对象,则可以考虑引入一个对象来增加耦合。您现在有了三个相互依赖的类,而不是两个。但是,这还不是整个系统。您还需要导入音轨,播放音轨,显示音轨等。向该混音添加一个以上的类可以忽略不计。

现在考虑需要增加对通过网络播放曲目的支持,而不仅仅是在本地。您只需要创建一个NetworkTrack符合相同接口的对象。没有Track对象,您将不得不在各处创建函数,例如:

addNetworkTrack(int no, string title, double duration, URL location)

这有效地使您的耦合增加了一倍,甚至不需要关心网络特定内容的模块,仍然能够对其进行跟踪,以便能够继续进行下去。

纹波效应测试是确定真实耦合量的好方法。我们所关心的是限制变更所影响的地方。


1
+无论如何切片,与原语的耦合仍在耦合。
JustinC 2013年

+1代表添加网址选项/涟漪效应。
user949300 2013年

4
+1有趣的读物也将是在野外DIP中的依赖倒置原则的讨论,在这种情况下,使用基本类型实际上被看作是使用值对象作为解决方案的原始痴迷 “气味” 。对我来说,听起来最好传递一个包含大量原始类型的Track对象……如果您想避免依赖于特定类/与特定类耦合,请使用接口。
Marjan Venema 2013年

接受答案是因为很好地解释了整个系统耦合与模块耦合之间的区别。
m3th0dman 2013年

10

我的建议是:

采用

addTrack( ITrack t )

但请确保这ITrack是一个接口,而不是具体的类。

相册不知道ITrack实现者的内部。它仅与所定义的合同有关ITrack

我认为这是产生最少耦合的解决方案。


1
我相信Track只是一个简单的bean /数据传输对象,在其中仅包含字段和其上的getter / setter。在这种情况下需要接口吗?
m3th0dman 2013年

6
需要?可能不会。暗示性的,是的。轨道的具体含义可以并且将会发展,但是消费类对它的要求可能不会。
JustinC

2
@ m3th0dman始终取决于抽象,而不取决于具体概念。无论Track愚蠢或聪明,都适用。Track是一种固结。ITrack接口是一个抽象。这样一来,只要您遵守,您将来就可以拥有各种轨道ITrack
TulainsCórdova13年

4
我同意这个想法,但是丢了“ I”前缀。摘自Robert Martin,第24页的Clean Code:“前面的I,在当今的一堆旧书中很常见,充其量会分散注意力,而在最坏的情况下则会提供太多信息。我不希望我的用户知道我正在向他们提供接口。”
本杰明·布鲁姆菲尔德2013年

1
@BenjaminBrumfield你是对的。我也不喜欢该前缀,尽管为清楚起见,我将在答案中留下。
TulainsCórdova13年

4

我认为第二个示例方法最有可能增加耦合,因为它最有可能实例化Track对象并将其存储在当前的Album对象中。(正如我在上面的评论中所建议的那样,我认为Album类在其内部某处具有Track类的概念是固有的。)

第一个示例方法假定Track在Album类之外被实例化,因此至少可以假定Track类的实例未与Album类耦合。

如果最佳实践表明我们从来没有一个类引用第二个类,则整个面向对象程序设计将被抛诸脑后。


我看不到对另一个类的隐式引用如何使其比对显式引用更具有耦合性。无论哪种方式,这两个类都是耦合的。我确实认为最好将耦合明确,但我认为这两种方式都不存在“更多”的耦合。
TMN

1
@TMN,额外的耦合在于我暗示第二个示例可能最终在内部创建新的Track对象。对象的实例与一个方法耦合,否则该方法应该只是将Track对象添加到Album对象中的某种列表中(这违反了单一职责原则)。如果需要更改创建Track的方式,则也需要更改addTrack()方法。在第一个示例中情况并非如此。
Derek

3

耦合只是尝试从代码中获得的众多方面之一。通过减少耦合,您不必改进程序。通常,这是最佳做法,但在这种情况下,为什么不Track应该知道?

通过使用Track要传递给的类,可以Album使代码更易于阅读,但是更重要的是,正如您提到的,您正在将静态参数列表转换为动态对象。这最终使您的界面更加动态。

您提到封装被破坏,但事实并非如此。 Album必须了解的内部Track,如果您不使用对象,Album则必须知道传递给它的每条信息,然后才能完全使用它。调用者还必须了解对象的内部Track,因为它必须构造一个Track对象,但是如果将其直接传递给方法,则调用者必须完全了解此信息。换句话说,如果封装的优点是不知道对象的内容,则在这种情况下不可能使用它,因为Album必须Track完全相同地使用信息。

您不希望使用的地方Track是是否Track包含不希望调用者访问的内部逻辑。换句话说,如果Album要使用使用您的库的程序员的类,Track那么如果您使用它来说,请不要让他使用,请调用一种方法将其持久化在数据库上。真正的问题在于接口与模型纠缠在一起。

要解决此问题,您需要将Track其分为接口组件和逻辑组件,创建两个单独的类。对于呼叫者而言,它Track变成一个轻量级,旨在保留信息并提供较小的优化(计算的数据和/或默认值)。在内部Album,您将使用一个名为的类TrackDAO来执行与将信息保存Track到数据库相关的繁重工作。

当然,这只是一个例子。我敢肯定这根本不是您的情况,因此请放心使用Track无罪感。只要记住在构造类时要牢记调用者,并在需要时创建接口。


3

两者都是正确的

addTrack( Track t ) 

更好(因为你已经argumented),而

addTrack( int no, String title, double duration ) 

少再加因为代码使用addTrack并不需要知道,有一个Track类。例如,可以重命名Track,而无需更新调用代码。

当您在谈论更具可读性/可维护性的代码时,本文在谈论耦合。耦合较少的代码不一定易于实现和理解。


见论点4;我看不出第二个因素耦合程度如何。
m3th0dman 2013年

3

低耦合并不意味着没有耦合。某些地方的某些事物必须了解代码库中其他地方的对象,并且减少对“自定义”对象的依赖性越多,则给出更改代码的原因就越多。您引用的作者通过第二个功能提倡的内容耦合度较低,但也较少面向对象,这与GRASP作为面向对象设计方法论的整个思想背道而驰。关键是如何将系统设计为对象及其相互作用的集合。避开它们就像在教您如何开车一样,说应该骑自行车。

相反,正确的途径是减少对具体对象的依赖,这就是“松散耦合”的理论。方法必须知道的确切具体类型越少越好。仅通过该语句,第一个选项实际上就没有那么多耦合了,因为采用简单类型的第二种方法必须知道所有这些简单类型。当然他们是内置的,并且该方法中的代码可能要照顾,但该方法的方法的调用者的签名,肯定也不会。更改与概念音轨相关的参数之一时,与将它们包含在Track对象(这是对象的指向;封装)中分开时相比,将需要进行更多更改。

再往前走,如果期望Track被更好地完成相同工作的东西所替代,则定义必要功能的接口可能就是ITrack。这可能允许使用不同的实现,例如“ AnalogTrack”,“ CdTrack”和“ Mp3Track”,这些实现提供了更特定于那些格式的附加信息,同时仍提供了ITrack的基本数据公开,从概念上讲代表了“轨道”;音频的有限子片段。类似地,Track可以是抽象基类,但是这要求您始终希望使用Track固有的实现。将其重新实现为BetterTrack,现在您必须更改预期的参数。

因此,黄金法则;程序及其代码组件将始终具有更改的理由。您不能编写一个程序,它永远不需要编辑已经编写的代码即可添加新内容或修改其行为。你的目标,在任何方法(GRASP,SOLID,任何其他缩写或流行语,你能想到的),仅仅是为了确定事情将会有时间改过来,并设计系统,使这些变化容易使尽可能(已翻译;触摸尽可能少的代码行,并尽可能减少超出预期更改范围的系统其他区域)。典型的例子,什么是最有可能的变化是一个追踪器将获得更多的数据成员addTrack()可能会或可能不会在意, 该Track将被BetterTrack取代。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.