# 编程思想 – 低耦合 作者:Ex 编程思想并不局限任何编程语言。 ## 耦合 耦合就是不同模块间互联程度度量。而模块呢,可以把它看成C语言当中的函数或者Java当中的类。在我们编程的时候要尽量做到模块独立。保持尽量松散的耦合。 ## 耦合的具体种类 ### 1. 非直接耦合 第一个非直接耦合是最优耦合形态。它的定义为两个模块之间要尽可能的独立。也就是一个模块不需要依赖另一个模块的存在。 ``` c #include void sub() { printf("hello world\n"); } int main() { sub(); return 0; } ``` 在这里面呢,有两个函数。一个是main函数可以把它看成`主模块`,另一个是子函数,可以看成`子模块`,子模块的功能也比较简单,就是打印出hello world,整个程序的流程就是主模块调用子模块打印出hello,word。这段程序中,主模块和子模块的耦合度就非常的低。因为主模块无论传递什么参数到子模块当中都不会改变子模块的的输出结果。 > 虽然非直接耦合是一种最优的耦合形态。但是这种情况很少出现,只能说这仅仅是一种理想状态,真实的情况往往模块之间是需要传递一些信息的。 ### 2. 数据耦合 `数据耦合也是很优秀的耦合,在我们写代码的时候也要尽量使用数据耦合`,他的定义是两模块之间通过参数交换数据,也就相当于我们编程当中的参数传递。 ``` c #include int add(int a, int b) { return a + b; } int main() { int a, b, c; a = 1; b = 2; c = add(a, b); printf("%d + %d = %d\n", a, b, c); return 0; } ``` 这段代码呢就是一段非常典型的数据耦合形态。用子模块实现两个数相加的功能。子模块的输出会随着参数的不同而改变。但他的程序流程是不会变的。 ### 3. 控制耦合 `控制耦合是耦合度比较差的耦合形态,写代码时应该尽量避免`,他的定义是两模块通过参数交换控制信息(`控制流`),也就是参数会影响模块的控制流程,下面举个例子。 ``` c #include int cal(int a, int b, char ch) { switch (ch) { case '+': return a + b; break; case '-': return a - b; break; } } int main() { int a, b, c, d; a = 1; b = 2; sign1: c = cal(a, b, '+'); sign2: d = cal(a, b, '-'); printf("%d + %d = %d\n", a, b, c); printf("%d - %d = %d\n", a, b, d); return 0; } ``` 如上所示,由于传入的控制参数不同,所以程序流程就不同,最终导致输出的结果也不同,这段代码的耦合度就比较高,平时写代码的时候应该尽量避免,但少数情况下不得不用控制耦合的话,也可以少量使用,比如C语言的`fopen()`函数,就是典型的控制耦合。 ``` plaintext NAME fopen, fdopen, freopen - stream open functions SYNOPSIS #include FILE *fopen(const char *pathname, const char *mode); FILE *fdopen(int fd, const char *mode); FILE *freopen(const char *pathname, const char *mode, FILE *stream); Feature Test Macro Requirements for glibc (see feature_test_macros(7)): fdopen(): _POSIX_C_SOURCE DESCRIPTION The fopen() function opens the file whose name is the string pointed to by pathname and associates a stream with it. The argument mode points to a string beginning with one of the follow‐ ing sequences (possibly followed by additional characters, as described below): r Open text file for reading. The stream is positioned at the beginning of the file. ....... ``` ### 4. 公共环境耦合 具体的定义是两个或多个模块通过一公共数据环境作用。 `公共环境耦合`分为两种情况 #### 1. 一模块发送数据,另一模块取数据,`等价数据耦合` 举个例子: ``` c #include char global_buf[0x100]; void read_buf() { puts("Output your content"); puts(global_buf); } void write_buf() { puts("Please input your content"); scanf("%256s",global_buf); } int main() { write_buf(); read_buf(); return 0; } ``` 上面的就是一个典型的`公共环境耦合`例子,写模块的时候遇到这种情况,最好是给相应的`公共环境耦合`模块最好充分的注释,否则一旦后期修改到global_buf,或者代码移植时没注意到`公共环境耦合`的话,将会出现很多问题,最好如下面这样写: ``` c #include char global_buf[0x100]; // 警告: // 依赖于全局变量 char global_buf[0x100] void read_buf() { puts("Output your content"); puts(global_buf); } // 警告: // 依赖于全局变量 char global_buf[0x100] void write_buf() { puts("Please input your content"); scanf("%256s",global_buf); } int main() { write_buf(); read_buf(); return 0; } ``` #### 2. 两模块既在公共环境送数据,又从里面取数据,介于数据耦合和控制耦合之间。 也就是上面的模块不仅能写公共环境数据,还能写数据。由于操作系统的异步性,很容易出现互斥等问题,应该尽量避免。 ``` c #include #include char global_buf[0x100]; // 警告: // 依赖于全局变量 char global_buf[0x100] void read_buf() { puts("Output your content"); puts(global_buf); } // 警告: // 依赖于全局变量 char global_buf[0x100] void write_buf() { puts("Please input your content"); // 写数据 scanf("%256s",global_buf); // 读数据 puts(global_buf); } int main() { write_buf(); read_buf(); return 0; } ``` ### 5. 内容耦合 `内容耦合`是耦合性最差的,写代码时应该杜绝这种情况发生。`内容耦合`也有4种情况,那么鄙人分别举四个例子来解释一下。 #### 1. 一模块访问另一模块内部数据 ``` java public class demo { public static void main(String[] arg) { Cal cal = new Cal(); int a, b, c; a = 1; b = 2; c = cal.calculate(a, b); System.out.println("" + a + " + " + b + " = " + c); } } class Cal { public char ch; // 初始化为加模块 Cal() { this.ch = '+'; } public int calculate(int a, int b) { switch (this.ch) { case '+': return a + b; case '-': return a - b; } return -1; } } ``` 正常情况下`Cal`模块是仅用来做加预算的,但是一旦他的内部数据被主模块访问并修改的话,就可能会出现程序员意料之外的错误。 ``` java public class demo { public static void main(String[] arg) { Cal cal = new Cal(); int a, b, c; a = 1; b = 2; // 修改Cal模块内部数据 cal.ch = '-'; c = cal.calculate(a, b); System.out.println("" + a + " + " + b + " = " + c); } } class Cal { public char ch; // 初始化为加模块 Cal() { this.ch = '+'; } public int calculate(int a, int b) { switch (this.ch) { case '+': return a + b; case '-': return a - b; } return -1; } } ``` 如上所示,由于Cal模块内部数据被修改,导致程序流的更改,导致输出的结果并不是我们想要的。结果如下所示: ``` plaintext ex@Ex:~/test/1$ javac demo.java ex@Ex:~/test/1$ java demo 1 + 2 = -1 ``` #### 2. 一模块不通过正常入口转到另一模块内部 最典型的例子就是goto语句,毫无规则的乱跳到某个地方,这就是为什么在编程的时候都杜绝用goto语句,因为他会让程序的耦合度过高。下面我举个例子来演示这种情况。 > `注意`:由于goto语句是不能跨函数使用的(可能就是为了防止耦合度过高),但是我们可以直接在C语言中嵌入汇编来实现。而且仅仅在Gcc上可以编译成功,msvc还是会报错。 ``` c # include void sub() { puts("start sub"); asm("sign:"); puts("end sub"); } int main() { puts("start main"); asm("jmp sign"); puts("end main"); return 0; } ``` 如上所示,主模块在调用子模块的时候,并不是通过正常的入口进入,而是直接调到子模块中间。执行结果如下: ``` plaintext ex@Ex:~/test/1$ gcc demo5.c ex@Ex:~/test/1$ ./a.out start main end sub ex@Ex:~/test/1$ ``` 可以看到程序确实超出了我们的意外,预期应该是以`end main`结尾的,结果却变成了`end sub`,导致整个程序流错乱。 #### 3. 两模块有部分程序代码重叠 举个简单的例子: ``` c #include void sub1() { printf("hello "); asm("jmp sign"); } void sub2() { asm("sign:"); printf("world\n"); } int main() { sub1(); return 0; } ``` 上面当中`printf("world\n");`就重叠了,这种情况多出现在汇编上,造成的严重后果就是,一旦更改了复用部分,可能会牵连多个模块,甚至整个程序流都会有变动。 #### 4. 一模块有多个入口 这个和`一模块不通过正常入口转到另一模块内部`是一个道理,这里我就不赘述了。 ## 编程建议 `尽量使用数据耦合,少用控制耦合,限制公共环境耦合,完全不用内容耦合。`