深入理解软件工程中的模块内聚与耦合原理,实现高内聚低耦合设计
[id_[id_1303186243]701127[id_1853136223]70]

[id_5[id_20[id_1614564583]546692]835142]
(清楚相关原理的读者,请直接看第二部分示例篇)
在软件工程领域,模块的内聚以及耦合是衡量模块化质量的标准之一。内聚是对模块功能强度的度量,也就是对一个模块内部各个元素彼此结合紧密程度的度量。如果一个模块内的各元素(比如语名之间、程序段之间)联系得越紧密,那么它的内聚性就越高。耦合则是对程序中各模块之间相互联系紧密程度的一种度量。各模块之间联系越紧密,其耦合性就越强。模块间的耦合程度高低取决于模块间接口的复杂情况、调用的方式以及传递的信息。
[id_17656198[id_458129054]]
所谓高内聚,指的是模块由相关性很强的代码构成。它只负责一项任务,也就是常说的单一责任原则。这样的模块,无论是在设计方面,还是在实现方面,亦或是在阅读方面,都能够体现出其保持专一性所带来的好处。
低耦合意味着模块之间应尽量独立存在,模块之间存在联系虽不可避免,但模块与模块之间的接口应尽量少且简单。这样一来,从整个程序中每一个模块的内部特征角度来看是高内聚,从程序中各个模块之间的关联关系角度来看是低耦合,它们共同对我们的设计提出了要求。
程序设计与软件工程发展过程中生成的诸多技术以及设计原则,均能够从内聚和耦合的视角来加以解读。对于 C 语言程序设计的初学者而言,依据当前对函数的理解所能够达到的水准,我们来探讨一下怎样做到高内聚且低耦合。
[id_961620190]
针对低耦合。非直接耦合的耦合程度最低。这种耦合指的是两个函数之间的联系完全通过共同的调用函数的控制和调用来实现。它的耦合度最弱,函数的独立性最强。然而,一组函数之间没有数据传递显然是不现实的。所以次之追求数据耦合。在数据耦合中,调用函数和被调用函数之间只传递简单的数据参数,比如采用值传递方式的函数。
特征耦合包括传指针并可修改指针指向的内存,以及不同函数打开同一文件进行操作。
有些函数在调用时,会通过形式参数传地址的方式。在函数体内,借助指针能够修改其指向的、作用域之外的存储单元。这样就构成了更强的耦合,这种耦合被称为特征耦合。在此情况下,让函数之间产生联系的是地址这样的特征标识。另外,存在两个函数有可能会打开同一个文件来进行操作,这也是特征耦合的一种形式。
外部耦合包括以下两种情况:一是访问同一全局变量,且不是通过参数表来传递全局变量信息;二是通过静态局部变量,以便在同一个程序的两次调用之间能够共享数据。
外部耦合是更强的耦合。在这种情况下,一组模块都会访问同一个全局变量,并且不会通过参数表来传递该全局变量的信息。当发现程序执行结果出现异常时,很难确切地定位到是在哪个函数中出现了差错。很多初学者认为参数传递比较麻烦,于是就将将要处理的数据尽可能地定义为全局变量。这样一来,函数之间的接口变得简单了,然而却形成了耦合性很强的结构。
在 C 语言里,能够借助静态局部变量,在同一程序的两次调用期间实现数据共享。这可被看作是一种外部耦合。然而,静态局部变量的作用范围局限于函数内部,其影响也仅在函数内部。与使用全局变量相比,这种耦合程度要弱很多很多。基于此,我们能够理解之前在使用全局变量、静态局部变量时所提出的“在合适的时候使用,不要滥用”的原则。
功能内聚意味着模块内所有元素的各个组成部分都是为完成同一个功能而存在的,它们共同完成一个单一的功能,这体现了高内聚的特点。
高内聚方面,功能内聚的内聚程度最高。模块内所有元素的各个组成部分都是为完成同一个功能而存在的,它们共同完成一个单一的功能,且模块已无法再分。这样的函数功能清晰且明确,通常出现在程序结构图的较低被调用层次上。
一个函数中,前一个处理元素的输出会成为后一个处理元素的输入,这体现了顺序内聚。
其次是顺序内聚,在一个函数中,各个处理元素都与同一个功能紧密相关。通常情况下,前一个处理元素的输出会成为后一个处理元素的输入。对于这样的函数,如果不会导致产生高耦合,那么可以将其拆分成两个函数来实现。
通信内聚指的是不同函数处理功能因为都对某一个公用数据进行访问而产生关联;信息内聚是指不同函数处理功能由于都访问某一个公用数据而发生关联。
有的函数,其不同处理功能因都访问某一个公用数据而产生关联,这种情况被称为通信内聚和信息内聚,内聚程度进一步降低。
一个函数内的各处理元素之间没有任何联系,只是碰巧被凑到了一起,这就是偶然内聚。
内聚程度低的情况无需一一列举。在最差的偶然内聚中,一个函数内的各处理元素之间毫无联系,仅仅是偶然地被组合在一起。
可以想像这样的模块,它做事没有条理,东一下西一下,就好像一个没有凝聚力的团伙一样,这样的情况对应的是低质量。总之,在解决问题划分函数的时候,要遵循“一个函数,一个功能”的原则,要尽量让模块达到功能内聚的状态。
要实现高内聚低耦合,关键在于写代码之前要花费一些时间进行精心设计。在接下来的例子里,会结合具体的问题,探讨如何把上述的这些因素考虑进来。
二、示例篇(输出200以内的完全平方数)
裘宗燕老师的《从问题到程序——程序设计与 C 语言引论》启发了本例。
输出的完全平方数有:1,4,9,16,25,36,49,64,81,100,121,144,169,196。每隔 5 个数据换行。此任务在一个 main 函数中完成。
方案1(高内聚,单模块实现方案)
//方案1:内聚性较高的单模块实现方案
#include
int main()
{
int m, num=0;
for (m = 1; m [id_1411962434] m <= 200; m++)
{
printf("%d ", m * m);
num++;
if (num[id_1041442502]5==0)
printf("\n");
}
return 0;
}
任务本身简单,在一个 main 函数中实现后,此函数的内聚程度接近功能内聚,且已经很高了。就任务而言,无需再进行分解。为让读者深入理解模块质量方面的技术,我们会尝试进一步提高内聚程序,接着考察耦合程度不同的各种解决方案。
要提高上述解决方案中函数(仅 main 一个函数)的内聚程度,我们来考察程序的功能,即“找出完全平方数并输出”。其中,“找出完全平方数”和“输出”本身就是两个不同的功能。进一步细分输出时,还有“要求 5 个数据在一行”的要求。这些功能的实现细节都集中在一个函数当中,由此可见,是有提升内聚程度的空间的。
在实现的应用里,几乎所有的处理都能分解成“输入-计算-输出”这种模式。优秀的解决方案通常至少要把这三个模块分别独立出来。对于“计算”模块来说,它的内部不再包含输入和输出,只专门接受输入的数据,计算完毕后就返回结果。当然,要是面对复杂的问题,在各个环节上或许还需要进一步分解。
我们接下来探讨这样一种方案,即把“找出完全平方数并输出”与“每 5 个数据后换行”这两个操作分别去实现。这种分解方式有利于提升内聚性。与此同时,我们需要关注分解之后的这两个模块之间的耦合程度。
现在将“找出完全平方数并输出”这一功能依旧放置在 main 函数中(若将其独立成为单独的函数也是可以的,但目前没有这个必要)。而“每 5 个数据后换行”的功能,设计一个名为 format 的函数,该函数每次调用就输出一个空格,用于分隔两个完全平方数,当调用到第 5 次时,输出的则是一个换行。
方案 2 属于数据耦合,通过函数参数值进行传递,而不传递返回值,这种方式不能更改外部的值,所以无法满足既定的需求。
这两个模块之间,需要传递一个“现在是第几次调用”的信息,不能采用耦合程度最松散的非直接耦合方式。我们对数据耦合进行了考虑,通过用简单形式参数传值,从而得到了方案 2。
//方案2:一个耦合度低,但不能完成功能要求的解决方案
#include
void format(int);
int main()
{
int m, num=0;
for (m = 1; m * m <= 200; m++)
{
printf("%d", m * m);
format(num);
}
return 0;
}
void format(int n)
{
n++;
if (n%5==0)
printf("\n");
else
printf(" ");
return;
}
编译运行结果:
1 4 9 16 25 36 49 64 81 100 121 144 169 196
在这个程序结构里,format 与 main 函数的耦合情况属于数据耦合。main 函数中定义了局部变量 num,在还未进行任何输出的时候,将其初值设定为 0 是合理的。当调用 format 时,把 num 传递过来的表示第几次输出(也就是第几个完全平方数)的形式参数 n 进行自增 1 操作,接着再对输出空格或换行进行控制。
分析和运行程序后发现,“每隔 5 个数据输出一个换行”的功能未实现。原因在于形式参数 n 在函数 format 内的改变,其所对应的实在参数 num 占据着不同的内存空间。n++进行修改的结果,对 num 没有产生任何影响,从而在下次调用时,丢失了“输出的是第几个”这一重要信息。
方案 3 属于数据耦合,也就是普通耦合。在这种耦合方式下,是通过简单形式参数传值,即进行函数参数和返回值值的传递。
一个补救的方法是,让 format 将变化后的 n 值当作返回值,然后把这个返回值再传回到 main 函数,这样就得到了如下方案 3 的程序。
可以将 num 的地址传递给 format 函数吗?或许这样会使耦合度较高呢!可参考方案 4。
//方案3:利用了返回值使耦合度增大,但功能得以实现的方案
#include
int format(int);
int main()
{
int m, num=0;
for (m = 1; m * m <= 200; m++)
{
printf("%d", m * m);
num = format(num);
}
return 0;
}
int format(int n)
{
n++;
if (n%5==0)
printf("\n");
else
printf(" ");
return n;
}
编译运行结果:
1 4 9 16 25
36 49 64 81 100
121 144 169 196
方案4(特征耦合【较高耦合】,传指针可函数内修改值)
维持原函数的返回值为 void,然后把参数改成传地址的方式,这样就得到了下面的方案 4。这个方案的耦合程度相对更高一些,不过其功能依然是可以实现的。
//方案4:传地址实现功能的方案,耦合度更大
#include
void format(int*);
int main()
{
int m, num=0;
for (m = 1; m * m <= 200; m++)
{
printf("%d", m * m);
format(&num);
}
return 0;
}
void format(int *p)
{
(*p)++;
if ((*p)%5==0)
printf("\n");
else
printf(" ");
return;
}
编译运行结果:
1 4 9 16 25
36 49 64 81 100
121 144 169 196
方案 5 是外部耦合且为高耦合,在函数内会直接修改全局变量。这种方案的程序规模稍大,可能带来的问题必须被高度重视。在实际应用中,要强调慎用全局变量,但不是完全不用。
)
有人想到了使用全局变量的办法。这样一来,能够把 num 设定为全局变量,num 的生存周期不再由函数调用所决定,它的值在函数调用期间也能保持不变(只要期间没有对其进行赋值操作),进而能够完成传递信息的任务。此时,由于 format 无需进行参数传递,所以可以将其设计为无参函数,这样就得到了如下方案 5 的程序。
//方案5:耦合度最高的全局变量方案
#include
void format();
int num=0;
int main()
{
int m ;
for (m = 1; m * m <= 200; m++)
{
printf("%d", m * m);
format();
}
return 0;
}
void format()
{
num++;
if (num%5==0)
printf("\n");
else
printf(" ");
return;
}
编译运行结果:
1 4 9 16 25
36 49 64 81 100
121 144 169 196
这是解决该问题耦合程度最高的一个方案。将 num 定义为外部变量,这表明若还有其他函数,num 能被任何函数修改。当出现 format 计数错误时,寻找错误会很困难,并且修改后可能会在其他地方引发错误。在如此短小的程序中,这种方案或许还可以接受,但当程序规模稍变大时,可能带来的问题必须予以高度重视。因此,在实际应用中,强调全局变量要慎用(不是不用)。
方案 6 中存在外部耦合,这种耦合程度较高。同时,函数内有局部静态变量,这些变量属于函数私用数据,其耦合程度比全局变量稍低,属于自耦合。
因为 num 是在 format 中应用的私用数据,只有 format 会在意这究竟是第几个数据,而 main 原本是无需关心的。基于此,可以考虑把 num 定义为 format 中的局部静态变量,从而得出方案 6 的程序。
//方案6:用静态局部变量,耦合度偏高但封装性最好的方案
#include
void format();
int main()
{
int m ;
for (m = 1; m * m <= 200; m++)
{
printf("%d", m * m);
format();
}
return 0;
}
void format()
{
static int num=0;
num++;
if (num%5==0)
printf("\n");
else
printf(" ");
return;
}
编译运行结果:
1 4 9 16 25
36 49 64 81 100
121 144 169 196
静态局部变量 num 的作用域局限于局部,它被定义在函数体中。在所有方案中,其封装性是最好的,这能够确保信息的隐蔽性,防止其他函数无意地进行越权访问。然而,num 的生存期是全局的,能够跨越函数的不同次调用,从而可以在两次调用之间传递信息,其耦合程度(自身与自身的耦合)相对较高。但正是这种特性,使得 main 函数和 format 函数的耦合达到了最理想的状态,既保证了功能的正确性,又保证了局部数据的安全性,充分展现了静态局部变量的优势。
总结(方案6、方案3 > 方案4 > 方案5)
总之,在处理一个问题时,有许多方案可供选择。方案 1 是可以接受的,不过希望通过提高内聚性来进行改进;方案 2 以简单的参数传值方式实现了低耦合,但遗憾的是无法完成功能;在其他方案中,针对这个问题,其选择的优先顺序为:
方案6、方案3 > 方案4 > 方案5
建议大家回顾前面的内容,思考这样排序的理由。在探讨各个方案的过程中,我们应体会到在提高程序设计能力时,要不断学习新的技术,懂得新的评判标准,这是一个不断拓宽眼界的过程。在以后的学习练习过程中,可以多思考一些方案。还能够从专业的视角去评判方案的好坏。最终能够达到的是,出手时呈现出的是最佳方案的专业水准。
参考文章:一例看懂C语言程序中的内聚和耦合