我们一直强调软件开发中要按照高内聚、低耦合的设计原则来做代码结构设计。c语言和c++不同,c语言面向过程、c++面向对象。
真正的项目中,要对业务升级,原来的业务函数需要保留,要保证老的功能继续维持,不能直接删除,这时候c语言面向过程,通常使用回调的方法。c++面向对象,要实现高内聚、低耦合,需要使用接口技术。
什么是耦合性
耦合性其实就是程序之间的相关性。程序之间绝对没有相关性是不可能的,否则也不可能在一个程序中启动,如下图:
编辑
这是一个Linux中socket TCP编程的程序流程图,在图中的TCP服务器端,socket()、bind()接口、listen()接口、accept()接口之间肯定存在着相关(就是要调用下一个接口程序必需先调用前一个接口),也就是耦合,否则整个TCP服务器端就建立不起来,以及改变了bind()中的传入的数据,比如端口号,那么接下来的listen()监听的端口,accept()接收连接的端口也会改变,所以它们之间有很强的相关性,属于紧耦合。
耦合的形式
(1)数据之间耦合
在同一个结构体或者类中,如:
typedef struct Person { int age; char* name; }Person; class Person { private: int age_m; bool m_setname; std::string m_name; };
在上面的结构体和类中,年龄和名字两个基本数据单元组合成了一个人数据单元,这两个数据之间就有了耦合,因为它们互相知道,在一些操作中可能需要互相配合操作,当然这两种数据耦合性是比较低的,但是m_setname是判断m_name是否存在的数据,所以这两个数据之间耦合性就高很多了。
(2)函数之间的耦合性
函数如果在一个类中也会互相存在耦合性,比如下面例子:
class Person { Public: int getAge(){return m_age;}; void setAge(int age){m_age= age;}; std::string getName(){return m_name;}; void setName(std::string name){m_name= name;}; Private: int m_age; std::string m_name; };
其中的getAge()和setAge()接口操作的是同一个数据,能够互相影响,存在着很明显的耦合,但是getName()和getAge()两个接口相关性就不明显的,但是也会存在耦合性,因为getName()能够访问的类中数据,getAge()也能访问,如果程序员编写代码不注意,也会把在两个接口中调用到了相同数据,互相造成了影响。
除了封装在一个类中的函数之间有耦合性,外部的函数也会根据业务需要产生耦合,比如刚开始说的网络编程的例子中,socket()、listen()、bind()、accept()之间就产生了很强的耦合。
以及在两个类中,比如:
class Fruit {}; class Apple:Fruit {}; class FruitFactory { Public: Furit* getFruit(){Fruit* fruit_p = new Apple(); return fruit_p; } }; class Person { Public: Void eatFruit(Fruit* furit); }; FruitFactory fruitFactory; Fruit* fruit = fruitFactory.getFruit(); Person person; if (fruit != NULL) { person.eatFruit(fruit); }
上面的FruitFactory和Person两个类之间产生了数据耦合,而getFruit()和eatFruit()两个接口之间也产生了耦合。
(3)数据与函数之间的耦合
从(2)中的程序也能看出,eatFruit()这个接口和Fruit这个数据产生了耦合,如果不先创建Fruit,那么接下来的eatFruit()操作也没有意义,如果强制调用,甚至可能造成程序崩溃,产生coredump。
上面例子的耦合还是比较明显的,有一些不明显的耦合,如下:
Speaker speaker;
speaker.PowerOn() ;
speaker.PlayMusic() ;
表面上是 PlayMusic()对PowerOn()有依赖性,是函数之间的耦合,但背后的原因是 PowerOn()函数让播放器处于通电状态:
PowerOn(){
this.isPowerOn = true;
}
//只有通了电,播放器才能正常播放音乐
PlayMusic() {
if(this.isPowerOn)
Play();
}
这两个函数是通过 this .isPowerOn 这个数据进行沟通的 。这本质上还是数据和函数之间的耦合。
如何降低耦合性
或者说怎么解耦?
(1)贯彻面向接口编码的原则
程序不可能没有改动的,但是尽量把改动放在一个模块的内部,接口不要变,就算需要改变,最好使用适配器模式增加一个适配程序。因为接口就是一个程序与外部的关联处,保持接口不变,就是保持该模块和外部模块的耦合性不变,这样才能保证它的可移植性可重用以及不被外部模块的修改而影响。
(2)保证一个模块的可测试(单元测试)
如果一个模块是可以单独进行单元测试的,意味着它可以移植到其他程序上,耦合性低。
(3)可以学习一下设计模式的设计思想。
(4)让模块对内有完整的逻辑
解耦的根本目的是拆除元素之间不必要的联系,一个核心原则就是让每个模块的逻辑独立而完整。其中包含两点,一是对内有完整的逻辑 , 而所依赖的外部资源尽可能是不变量;二是对外体现的特性也是“不变量”(或者尽可能做到不变量),让别人可以放心地依赖我。有的函数光明磊落,它和外界数据的沟通仅限于函数的参数和返回值,那么这种函数给人的感觉可以用两个字形容:靠谱。它把自己所需要的数据都明确标识在参数列表里,把自己能提供的全集中在返回值里。如果你需要的某项数据不在参数里,你就会侬赖上别人,因为你多半需要指名道姓地标明某个第三方来特供;同理,如果你提供的数据不全在返回值和参数里,别人会依赖上你 。有的函数让人觉得神秘莫测,规律难寻:它所需要的数据不全部体现在参数列表里,有的隐藏在函数内部,这种不可靠的变量行为很难预测;它的产出也不集中在返回值,而可能是修改了藏在某个不起眼角落里的资源。这样的函数需要人们在使用过程中和它不断地磨合,才能掌握它的特性。前者使用起来放心,而且是可移植、可复用的,后者使用时需要小心翼翼 ,而且很难移植。
C语言为例:
软件通常有后台日志的记录功能,用log函数实现,主业务用business函数表示:
void log() { printf("Logging...\n"); } void business() { while(1) { sleep(1); printf("Deal Business...\n"); log(); } } int main() { business(); return 0; }
现在需要对后台日志功能进行升级,该如何实现?
一般人的想法是这样:再写一个函数log2,然后business中log改为log2,这样不就可以了?
但是你想想,主业务代码怎能轻易改动?因为一个小小的功能而要改变主要的业务代码,这样不是显得智商很捉急?
换一种思路,使用回调:
#include <stdio.h> #include <unistd.h> void log1() { printf("1 Logging...\n"); } void log2() { printf("2 Logging...\n"); } void business( void (*f)() ) { while(1) { sleep(1); printf("Deal Business...\n"); f(); } } int main() { business(log1); return 0; }
business函数接受一个函数指针,该指针指向的函数没有参数,返回值为void,符合log函数的原型。business中只要f()即可调用相应的函数。
当需要使用log1时,向business传log1、要使用升级后的log2时,传入log2即可。
C++为例:
C++中强调面向对象的思想。
#include <iostream> using namespace std; class Log { public: void log() { cout << "logging..." << endl; } }; class Business { private: Log *l; public: Business(Log *l = NULL) {} void business() { while(1) { sleep(1); cout << "Deal Business..." << endl; l->log(); } } }; int main() { Business b(new Log); b.business(); return 0; }
现在,我们需要对后台日志功能升级,怎么做?有人想到了C++中的重载,在Log类中重载一个函数log2;
也有人想到了继承Log类,覆写log函数等等,但是这几种方法,都需要对Business类中的代码进行变动。如何解决呢?于是C++中的接口技术就派上用处了。
记住,接口强调的是方法,接口里的方法定义为纯虚函数,接口不能实例化、也不需要实例化,需要接口里的功能的类只需要继承该接口即可!下面给出示例:
#include <iostream> using namespace std; class Log { public: virtual void log() = 0;//纯虚函数 }; class Log1 : public Log//继承接口 { public: void log() { cout << "1 logging..." << endl; } }; class Log2 : public Log//继承接口 { public: void log() { cout << "2 logging..." << endl; } }; class Business { public: void business(Log * f)//函数参数只要Log指针,具体传入的是Log1还是Log2的实例,由多态进行实现 { while(1) { sleep(1); cout << "Deal Business..." << endl; f->log(); } } }; int main() { Business b; b.business(new Log2);//会调用Log2类中的log日志函数! return 0; }
此时,对日志业务升级就不会影响business的代码了,只需将不同的日志实例化传入business中即可。