【函数设计】

计算机的函数,是一个固定的一个程序段,或称其为一个子程序,它在可以实现固定运算功能的同时,还带有一个入口和一个出口,所谓的入口,就是函数所带的各个参数,我们可以通过这个入口,把函数的参数值代入子程序,供计算机处理;所谓出口,就是指函数的函数值,在计算机求得之后,由此口带回给调用它的程序。

一个较大的程序一般应分为若干个程序块,每一个模块用来实现一个特定的功能。所有的高级语言中都有子程序这个概念,用子程序实现模块的功能。在C语言中,子程序的作用是由一个主函数和若干个函数构成。由主函数调用其他函数,其他函数也可以互相调用。同一个函数可以被一个或多个函数调用任意多次。
在程序设计中,常将一些常用的功能模块编写成函数,放在函数库中供公共选用。要善于利用函数,以减少重复编写程序段的工作量。

下面是一个编写的不好的函数

Void  HandleStuff(CORP_DATA* pInputRec, int CrntQtr, EMP_DATA  EmpRec,
                 float* pEstimRevenue,  float YTDRevenue, int ScreenX, 
                 int ScreenY,COLOR_TYPE * pNewColor, COLOR_TYPE PrevColor, 
		         STATUS_TYPE* pStatus , int ExpenseType) 
{
    int i;
	for( i = 1; i<= 100; i++){
		pInputRec->revenue[i] = 0;
		pInputRec->expense[i] = corpExpense [CrntQtr][i];
	}
	 UpdateCorpDatabase( EmpRec ) ;
	*pEstimRevenue  = YTDRevenue * 4.0 /(( float )CrntQtr);
	*pNewColor  = PrevColor;
	*pStatus  = Success;
	if( ExpenseType == 1){
        Profit[i] =Revenue[i] - Expense.Type1[i];
    }
    else if( ExpenseType == 2){ 
        Profit[i] = Revenue[i] - Expense.Type2[i];
    }
    else (if ExpenseType == 3){ 
	    Profit[i] = Revenue[i] - Expense.Type3[i];
    }
}

函数的缺点:

①函数的名字不好,不能说明函数是干什么的
②函数进行了全局变量的读写操作
- CorpExpense、profit都是全局变量
- 对全局变量的操作应使用专门的存取函数
③函数的功能不是单一的
- 它初始化了某些变量;对一个数据库进行写操作;又进行了某些计算工作。而这些功能之间又看不出任何联系
④函数中没有采取预防非法数据的措施
- 如果CrntQtr的值为“0”,那么,表达式YTDRevenue* 4.0/real(CrntQtr)就会出现被零除的错误
- 指针没有检查是否为空
⑤在函数中仅使用了CORP-DATA型参数的两个域,却传入了整个结构体变量
- 是否需要传递结构体变量需根据实际情况来定,比如计算某个点到自车的距离的函数,只传入必要的经纬度就可以了,不要传一个既有名称、读音等又有经纬度的PointInfo结构体变量。
- 一定要传结构体变量的话务必传递指针或引用。
⑥函数中的一些参数没有使用过 :ScreenX和ScreenY
⑦函数中的参数太多(不要多于7个)
⑧函数中使用了几个常数(神秘数字): 100,4.0,2和 3

函数设计概述

函数设计主要在详细设计中做

 创建函数的理由

  • 降低复杂性
  • 避免重复代码段
  • 限制改动带来的影响(改动一个函数的实现不会影响其它代码)
  • 隐含逻辑(比如排序)
  • 隐含数据结构(从DB数据中取得“版本号”)
  • 改进性能(可以优化某个函数算法来改进)
  • 实行集中控制(比如集中控制内存的申请)
  • 代码段复用
  • 改善某一代码段的可读性(min(),max())
  • 改善可移植性(分隔硬件依赖和OS依赖的代码)
  • 分隔复杂操作(繁琐的算法、通信协议等)
  • 独立非标准语言函数的使用
  • 简化复杂的布尔测试(比如IsSuccess())

大原则——内聚性

内聚性是指一个函数内的各种操作之间联系的紧密程度
可以接受的内聚性

  • 功能内聚性
  • 顺序内聚性
  • 通讯内聚性
  • 临时内聚性

不可接受的内聚性

  • 过程内聚性
  • 逻辑内聚性
  • 偶然内聚性

函数设计的目标:强内聚

可以接受的内聚性

功能内聚性

  • 函数中的所有操作都是为了完成一个单一功能。功能可以有大有小,但必须单一。“GetPersonalInfo”是一个单一的功能,“GetName”是单一的,“GetBirthday”是单一的,但“GetNameAndBirthday”就不是单一的功能了。
  • 举例:Sin(),GetCustomerName(),EraseFile()等
  • 设计某个函数时,功能内聚性是首选。
  • 函数名字的选取很重要,比如处理某项事务有如下步骤:读取用户的确认信息;增加一条记录到数据库中;计数器加1,进行以上步骤的函数取名为ConfirmEntryAndAdjustData时,此函数仅具有偶然内聚性;取名为CompleteTransaction时,此函数具有功能内聚性。

顺序内聚性

  • 特点:函数内的操作不能形成完整的功能,只是包含需要按特定顺序进行的、逐步分享数据的一些操作
  • 举例:下面五个操作完成一个功能:打开文件、读文件、进行两个计算、输出结果、关闭文件。如果DoStep1()打开文件、读文件和计算操作,而DoStep2()进行输出结果和关闭文件操作。Dostep1和DoStep2只是把步骤分隔开来,并没有产生出独立的功能,所以具有顺序内聚性。
  • 实际设计当中,把一个功能分成若干个步骤,需要有充足的理由。

通讯内聚性

  • 特点:函数中的若干个操作只是使用相同数据,而没有其它任何联系
  • 举例:GetNameandChangePhoneNumber(),它取得的Name和PhoneNumber在同一个用户记录中,GetName和ChangePhoneNumber之间没有其他的关系。
  • 有比较充分的理由才可以接受此内聚性,比如GetName的同时ChangePhoneNumber可以明显改善性能,不然的话还是推荐拆成GetName和ChangePhoneNumber两个函数,这样两个函数就具有功能内聚性了。

临时内聚性

  • 特点:函数的几个操作因为同时执行才被放入同一个函数
  • 例如:Startup(),CompleteNewEmployee(),Shutdown()等
  • 又比如到达目的地的操作(删除目的地;删除经路;停止引导等),可以在一个函数中完成,因为这些操作必须同时执行。

不可接受的内聚性

过程内聚性

  • 特点:函数中的操作是按某一特定顺序进行的,却不使用相同的数据。
  • 举例:按一定的顺序打印报告的函数,打印的内容包括销售收入、开支、雇员电话表。各操作之间并不共享数据。

逻辑内聚性

  • 特点:函数的几个操作之间没有任何联系,只是用传进来的控制标志来决定执行哪些操作,这些操作往往包括在一个很大的if“或者case语句中。
  • 举例:函数InputAll()的输入内容可能是用户名字、雇员时间卡信息或者库存数据,至于到底是其中的哪一个,则由传入函数的控制标志决定。

偶然内聚性

  • 特点:函数中的各操作之间无任何联系,也叫作“无内聚性”
  • 举例:本文中开头的那个不好的函数
     

内聚性实例

实例1:一个按给出的生日计算雇员年龄和退休时间的函数
分析:

  • 如果它是利用所计算的年龄来确定雇员将要退休的时间,那么它就具有顺序内聚性;而如果它是分别计算年龄和退休时间的,但使用相同生日数据,那它就只具有通讯内聚性。

如何改进成功能内聚性呢?

  • 可以分别建立两个函数,一个根据生日计算年龄,另外一个根据生日确定退休时间,确定退休时间函数将调用计算年龄的函数,这样,它们就都是功能内聚性的,而且其它函数也可以调用其中任何一个函数,或这两个都调用


实例2:一个函数将打印季度开支报告、月份开支报告和日开支报告,具体打印哪一个,将由传入的控制标志决定
分析:

  • 具有逻辑内聚性

如何改进成功能内聚性呢?

  • 建立三个函数:一个打印季度报告,一个打印月报告、一个打印日报告,改进原来的函数,让它根据传进去的控制标志来调用这三个函数之一。调用函数将只有调用代码而没有自己的计算代码,因而具有功能内聚性;而三个被调用函数也显然是功能内聚性的


实例3:一个函数先读取雇员的名字,然后是地址,最后是它的电话号码。这种顺序是用户要求的。另外一个函数将读取关于雇员的其它信息。
分析:

  • 如果雇员信息放在一个记录中,是顺序内聚;否则是过程内聚

如何改进成功能内聚性?

  • 把它分为几个部分,并把这几部分分别放入程序中。要保证调用程序的功能是单一、完善的。调用程序应该是诸如GetEmployeeData()这样的函数,而不该是像GetFirstPartOfEmployeeData()这类的函数。可能还要改动其余读取数据的函数。

实例4:比如一个函数进行某种复杂计算的前5个操作,并把中间结果返回到调用函数。由于5项操作可能要用好几个小时,因此当系统瘫痪时,函数要把中间结果存入一个文件中,函数还要检查磁盘,以确定其是否有足够空间来存储最后计算结果,并把磁盘状态和中间结果返回到调用程序
分析:

  • 原来的函数是由一系列令人莫名奇妙的操作组成的,与功能内聚性相距甚远

如何改进成功能内聚性?

  • 调用函数ComputeExtravagantNumber()应该调用几个独立的函数,不应该把中间结果写入一个文件,也决不该为后来的操作检查磁盘剩余空间,它所作的就仅限于调用其它具有单一功能的函数而已。改进这个设计,将至少影响到一到两个层次上的程序, 对于这项任务的较好设计,如下图所示 

 内聚性总结

共享数据、各个操作都是为了完成函数的功能而存在——功能内聚
共享数据、各个操作顺序进行——顺序内聚
共享数据、各个操作之间没有联系——通讯内聚
不共享数据、各个操作同时进行——临时内聚
不共享数据、根据控制标记决定做哪个操作——逻辑内聚
不共享数据、各个操作顺序进行——过程内聚
不共享数据、操作之间没有任何关系——偶然内聚
 

大原则——耦合性

耦合性是指两个函数之间联系的紧密程度 
函数与其它函数之间的联系应该是显著、松散和灵活的,函数应该容易被其他函数调用

  • 函数对外的接口包括参数、函数中使用的全局变量和数据库或文件等

耦合标准
-- 耦合规模

  • 两个函数之间联系的数量越少越好:参数越少越好,使用的全局变量越少越好,使用数据库和文件的范围越少越好

-- 显著性

  • 两个函数之间联系越显著越好,使用参数最显著,其次是全局变量,最后的数据库或文件

-- 灵活性

  • 改变两个函数之间联系越容易越好

耦合层次

简单数据耦合

  • 两个函数之间传递的数据是非结构化的,并且全部都是通过参数表进行的
  • 这通常称作“正常耦合”,这也是一种最好的耦合 

结构数据耦合

  • 两个函数之间传递的数据是结构化的,并且是通过参数表实现传递的
  • 如果使用恰当的话,这种耦合也不错

全局数据耦合

  • 两个函数使用同一个全局数据
  • 如果所使用的数据是只读的,那么这种耦合还是可以忍受的,但是,总的来说,全局耦合是不受欢迎的

控制耦合

  • 一个函数通过传入另一个函数的数据通知它该作什么
  • 控制耦合是令人不快的, 因为它往往与逻辑内聚性联在一起,并且,通常都要求调用者了解被调函数的内容与结构
     

耦合的例子

Tan(float degree)

  • 简单数据耦合。

一个函数向另一个函数传递姓名、住址、电话号码、生日和身份证号码等五个变量

  • 简单数据耦合。

一个函数向另一个函数传递变量EmpRec,,EmpRec是一个结构化的变量,包括姓名、住址、生日等等五个方面的数据

  • 结构数据耦合。
  • 传递结构体变量时务必传递指针或引用。

一个函数向另一个函数传递控制标志,通知它到底是打印月报表、季度报表还是年度

  • 控制耦合

一个函数把雇员识别卡传递给另一个函数,两个函数都利用这个识别卡从一个全局表中读取雇员的名字。 

  • 全局数据耦合

大原则——封装(信息隐藏)

封装有两层含义

  • 为了隐藏一些信息,往往会形成一个或多个函数
  • 函数的名字和参数应该尽量少的暴露实现的细节,这样可以在不影响用户的情况下方便地修改函数的实现方式。getNamefromDISC不太好,有可能以后从硬盘读数据了。

常见需要隐藏的信息
-- 容易被改动的区域(比如说解析数据格式,可以用函数封装起来)

  • 对硬件有依赖的地方
  • 输入和输出 
  • 非标准语言特性
  • 状态变量
  1. 不要使用逻辑型变量作为状态变量,应使用枚举型变量 
  2. 使用存取函数检查变量,而不要对其直接检查(比如IsOnRoute())
  • 数据规模限制(某些代码因数据量的多少而不同)
  • 商业规则 
  • 预计得到的其他改动 

-- 复杂的数据
-- 复杂的逻辑

小细节——函数名

函数名要有意义,要能描述函数所做的一切 

  • 在函数名字中,应描述所有输出结果及其附加结果 
  • 如果很难为一个函数命名,就需要考虑这个函数的功能是不是有问题

实现行为的过程或函数要使用动词或动词短语

  • 如PrintReport(), CheckOrder()

如果函数只有一个返回值,可以用返回值来描述

  • 如Cos(),  CurrentPenCount()
  • 如果函数返回布尔值,可以在形容词或名词前加Is或Has,如IsPrinterReady(), HasFirstName()

命名约定

  • 公开的接口函数和全局名字空间的函数选择SmallTalk风格的命名原则(一种首字母大写,字间直接相连而无分隔符的书写风格),如IsPrinterReady()
  • 私有的函数使用C++ 草拟标准工作底稿使用的约定(全部为小写字符、字间以下划线分隔),如is_printer_ready()
  • 编码规范中有对命名的详细规定。
     

小细节——参数

函数的参数的命名要有意义

  • stringCopy( char* str1, char* str2); /*这样的参数命名不好,很难区分*/

函数参数的顺序要合理

  • 按照输入一修改一输出的顺序排列参数,但是首先要符合使用习惯

         /*这个参数顺序不符合使用习惯,很容易出错*/

          StringCopy( char*strSource, char* strDestination)

          因为我们习惯这样使用:stringCopy( str, “hello”)

  • 如果几个函数中使用了相似的参数,应使用相同的顺序
  • 如strncpy和memncpy
  • 把状态和“错误”变量放在最后

参数的个数不要太多,尽量控制在7个以内

  • 如果参数太多,在使用时容易将参数类型和顺序搞错
  • 参数太多,有时也意味着函数功能划分不好

使用所有的参数,删除没有用到的参数
对参数的限制越少越好,但是如果对参数的取值有限制,一定要加上注释来说明
结构体如果很大,要用指针或引用传递

  • 参数太大,占用的堆栈空间大,调用的效率也低

如果参数是指针,且仅作输入用,则应在类型前加const,以防止该指针在函数体内被意外修改
 

小细节——返回值

显式声明函数的返回值类型

  • 原理:如果不显示的声明返回值类型,不同的编译器可以给与不同的解释
  • 如:TestFunc()最好能显示的声明为int TestFunc()

函数名字和返回值类型在语义上不可冲突

  • int getChar()就是一个不好的设计

不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用return返回
有时候函数原本不需要返回值,但为了增加灵活性(如支持链式表达),可以附加返回值

  • 例如 char *strcpy(char *strDest,const char *strSrc)

函数的每种出错返回值的意义要清晰、明了、准确,防止使用者误用、理解错误或忽视错误返回码
 

小细节——函数内部设计

函数体的规模不要太大,否则难以理解,也容易出错

  • 一个函数的规模不要超过350行。
  • 如果太大了应该根据前面讲过的大原则进行拆分。

资源管理:谁申请谁释放

  • 除非函数的功能就是申请资源(如malloc, openfile等函数),否则尽量不要在函数内部申请资源并返回资源的句炳或指针,如申请内存返回内存指针,打开文件返回文件指针等

Void GetData( struct Sample **ppData); //不好
Void GetData( struct Sample *pData);// OK

  • 由调用者申请的资源应该由调用者释放,不要在函数内部随便释放调用者申请的资源

Void ReadData( FILE* dataFile, char* buf){
        if( …){
        fclose( dataFile);   //关闭了调用者打开的文件,不好
       }
       …
}
不要修改调用者的数据
Struct Sample{
    int data1;
    int data2;
};
int  Sum(Struct Sample* pInputRec)
{
    if( pInputRec->data1 < 0)
          pInputRec->data1 = 0;  //这样做不好
    return pInputRec->data1 + pInputRec->data2;
}
int  Sum(Struct Sample* pInputRec)
{
    // 修改成这样就好多了
    int temp =pInputRec->data1 >=0)? pInputRec->data1:0;
    return temp + pInputRec->data2;
}
合理使用全局变量

  • 使用全局变量前,需要考虑是否真的有必要使用。
  • 确定要使用全局变量之后,应该意识到其带来的风险并避免。

全局变量的缺点

  • 全局数据的疏忽改变。你可以会在某处改变全局变量的值,而在别处又会错误地以为它仍保持着原值

TheAnswer = GetTheAnswer();——TheAnswer是一个全局变量
OtherAnswer = GetOtherAnswer();——GetOtherAnswer 改变了TheAnswer
AverageAnswer = (TheAnswer + OtherAnswer)/2—AverageAnswer错误

  • 伴随全局变量的奇怪的别名问题 

Void WriteGlobal (int& InputVar)
{
     GlobalVar = lnputVar + 1;
     cout<<“input Varinble ”<<InputVar;
     cout<<“Global Varinble “<<GlobalVar;
 }
GlobleVar = 1;
WriteGlobal( GlobleVar); 
最后的输出结果是input Varinble 2; Global Varinble 2。

  • 有全局数据的代码重入问题(重入:函数没有执行完成,由于外部因素或内部调用,又一次进入该函数执行。strtok函数的ANSI C标准版本就是非可重入的,因为其使用了静态变量来保存上一次查找的结果)
  • 全局数据妨碍代码复用
  • 全局变量会损害模块性和可管理性

怎样降低使用全局数据的危险

  1. 先使所有变量都成为局部的,然后再根据需要把其中某一些改为全局变量
  2. 全局变量加g_前缀,如g_theAnswer
  3. 不要通过把数据放入庞大的结构化变量,同时又到处传递它来掩盖你使用了全局变量的事实 
  4. 用存取函数来代替全局数据 
  • 要求所有使用全局变量的函数使用存取函数
  • 不要把所有的全局数据都放人同一个模块中 
  • 在存取函数中建立某种程度的抽象 

Node = node->next;    // 直接使用全局变量
Node = nextEmployee(node); // 比较抽象,容易理解

  • 把对数据的所有存取保持在同一抽象水平上 

AddEvent(Event) 
 EventCount = EventCount-l // 和上一个语句的抽象程度不同
 

函数检查表

总体问题

  • 创建函数的理由充分吗?
  • 如果把一个函数中的某些部分独立成另一个函数会更好的话,你这样做了吗?
  • 函数的命名合适么?函数的名称是否描述了它做的所有工作?
  • 函数的内聚性强么?它是只做一件工作并做得很好吗?
  • 函数的耦合是不是松散的?两个函数之间的联系是不是小规模、密切、可见和灵活的?
  • 函数的长度合适么? 

参数

  • 程序中参数的排列合理吗?与相似函数中的参数排列顺序匹配吗?
  • 接口假设说明了吗?
  • 程序中参数个数是不是7个或者更少?
  • 是否只传递了结构化变量中另一个函数用得到的部分?
  • 是否用到了每一个输入参数?是否用到了每一个输出参数? 

全局变量

  • 是否是在迫不得已的情况下,才使某些变量成为全局的?
  • 命名约定是否对局部、模块和全局变量进行了区分?
  • 是否给所有全局变量都加了注释?
  • 程序中是否不含有伪全局变量——传往各个函数的庞大而臃肿的数据结构?
  • 是否用存取函数来代替了全局数据?
  • 是把存取函数和数据组织成模块而不是随意归成一堆的吗?
  • 存取函数是否有一定程度的抽象?
  • 所有相互有联系的存取函数,其抽象程度都是一致的吗?
     

参考书

Code Complete (代码大全)
The Practice of Programming (程序设计实践)
高质量C/C++编程指南
 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值