0
点赞
收藏
分享

微信扫一扫

你的代码是否按照高内聚、低耦合的原则来设计的?

       我们一直强调软件开发中要按照高内聚、低耦合的设计原则来做代码结构设计。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中即可。

举报

相关推荐

0 条评论