软件设计启发法之——保持松散耦合

什么是耦合?
什么是松散耦合?
为什么需要松散耦合?
怎么才能做到松散耦合?
耦合的种类有哪些?

带着这些问题,一起探索:软件设计启发法之——保持松散耦合。


什么是耦合?

耦合的概念:

耦合描述了一个类或例程与其他类或例程之间的紧密程度。

什么是松散耦合?

松散耦合的概念:

创建与其他类或例程具有小、直接、可见和灵活关系的类或例程,这被称为“松散耦合”。


为什么需要松散耦合?

想象一下火车车厢之间的连接(耦合):

火车车厢通过相对的钩子连接在一起,当推在一起时,这些钩子会闩锁。连接两个车厢很容易——只需将车厢推到一起即可。但想象一下,如果你必须把某种东西拧在一起,或者连接一组电线,或者如果你只能将某些类型的车厢连接到某些其他类型的车厢,那将是多么困难。

火车车厢之间的连接(耦合)之所以有效,是因为它尽可能简单。

在软件开发中,模块之间的良好耦合足够松散,那么一个模块就可以很容易地被其他模块使用。所以,在软件开发中,要尽可能的让模块之间的连接也变得简单。


怎么才能做到松散耦合?

以下是用于评估模块之间耦合的几个标准:

松散耦合的标准
  • 大小(Size

    大小指模块之间的连接数。

    对耦合度来说,小就是美,因为将其他模块连接到具有较小接口的模块的工作量更少。

    使用一个参数的例程与调用它的模块的耦合比使用六个参数的例程更松散。有着四个定义良好的pulic方法与调用它的模块的耦合比有着37个spulic方法的类更松散。

  • 可见性(Visibility

    可见性是指两个模块之间连接的显著程度。

    在参数列表中传递数据是一种明显的连接,具有很好的可见性,因此是好的设计。修改全局数据以便另一个模块可以使用该数据是一种“偷偷摸摸”的连接,没有很好的可见性,因此是不好的设计。用文档记录全局数据连接让它更明显,会变得更好一些。

  • 灵活性(Flexibility

    灵活性是指你修改模块之间的连接有多容易。

    理想情况下,你需要的东西更像是电脑上的 USB 连接器,而不是裸线和焊枪。

    灵活性部分是其他耦合特性的产物,但也有一点不同。

    假设有一个例程,在给定招聘日期和职位的情况下查找员工(employee)每年获得的假期数量。将例程命名为 LookupVacationBenefit()。假设在另一个模块中,有一个包含招聘日期和职位等内容的员工(employee)对象,并且该模块将该对象传递给 LookupVacationBenefit()

    从其他标准的角度来看,这两个模块看起来是松散耦合的。两个模块之间连接的employee对象是可见的并且这里只有一个连接。现在假设需要从第三个没有emplyee对象但有招聘日期和职位的模块中使用 LookupVacationBenefit()。突然地, LookupVacationBenefit() 看起来不太友好,不愿意与新模块关联。

    第三个模块想要使用 LookupVacationBenefit()例程就必须知道employee类的存在。它可以仿造一个只有两个字段的employee对象,但那需要了解 LookupVacationBenefit()内部的实现,也就是说需要知道哪些是被使用的字段。这样的解决方案是杂乱并且丑陋的。第二个选择是修改 LookupVacationBenefit()例程,让它只接收招聘日期和职位而不是整个employee对象。

    显然,这里采用第二种选择是更灵活的。

    简而言之,其他模块越容易调用一个模块,它就越松散耦合,这是很好的,因为它更灵活,更易于维护。


耦合的种类

下面是一些在系统中会遇到的常见的耦合:

  • 简单数据参数耦合(Simple-data-parameter coupling

    如果两个模块之间传递的所有数据都是原始数据类型(primitive),并且所有数据都通过参数列表传递,则两个模块是简单数据参数耦合的。这种耦合是正常的,可以接受的。

  • 简单对象耦合(Simple-object coupling

    如果一个模块实例化了一个对象,那么它就是与该对象耦合的简单对象。这种耦合是好的。

  • 对象参数耦合(Object-parameter coupling

    如果 Object1 要求 Object2 向其传递 Object3,则两个模块是相互对象参数耦合的。这种耦合比 Object1 要求 Object2 只传递原始数据类型更紧密,因为它要求 Object2 了解 Object3。

  • 语义耦合(Semantic coupling

    最潜伏的耦合发生在一个模块不使用另一个模块的某些语法元素,而是使用另一个模块内部工作的一些语义知识时。下面是一些例子:

    • Module1Module2传递了一个控制标志(control flag),告诉Module2应该做什么。这种方法需要 Module1Module2 的内部工作做出假设,即 Module2 将如何处理控制标志。如果 Module2 为控制标志定义了特定的数据类型(枚举类型或对象),则这种用法可能是可以的。
    • Module2Module1修改全局数据之后使用全局数据。这种方法需要Module2假设Module1以它需要被修改的方式修改全局数据,并且Module1在正确的时机被调用。
    • Module1的接口表明它的Module1.Initialize() 例程应该在 Module1.routine() 例程调用之前调用。Module2知道Module1.routine()无论如何都会调用Module1.Initialize(),所以它只实例化Module1调用*Module1.routine()例程而没有首先调用Module1.Initialize()*例程。
    • Module1Module2传递对象。因为Module1知道Module2只使用对象7个方法中的三个,它只部分初始化 对象——使用那三个方法需要的特定的数据。
    • Module1Module2传递BaseObject(基对象)。因为Module2知道Module1实际传递的是DerivedObject(派生对象),它将BaseObject强制转换成DerivedObject,并且调用特定于DerivedObject对象的方法。

    语义耦合是危险的,因为更改被使用模块中的代码可能会以编译器完全无法检测到的方式破坏调用它的模块。当出现这样的代码破坏时,它以微妙的方式破坏,这些方式似乎与被使用的模块中所做的更改无关,这会将调试变得非常困难。


总结

松散耦合的关键在于一个有效的模块提供了额外的抽象级别——一旦你写了它,你可以认为它是理所当然的。它减少了整体程序复杂性,并且允许你一次只专注在一件事情上。如果使用一个模块需要你一次专注在不止一件事情上——对其内部工作的了解、对全局数据的修改、不确定的功能——那么该模块的抽象能力已经丧失,该模块帮助管理复杂性的能力也将降低或彻底消除。

类和例程是降低复杂性首要的智能工具。如果他们没有让你的工作变得更简单,那么他们就没有做好他们的工作。


最后

以上仅作为笔记,内容来自《Code Complete 2: A Practical Handbook of Software Construction》一书。

上面的内容看起来很可能有点奇怪,如果可以阅读英文的话,还是建议去找到原版书读,其实英文原文确实是很有趣的!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值