前言
软件开发,在很多人的眼里是一件艰苦、困难的事情,在软件开发中发生诸多问题,如:
1.“改一出两”或客户处发生但开发环境下不再现的Bug;
2.客户“随心所欲”的更改要求,而现有的设计又无法满足。
3.进行新项目开发时,由于没有一个好的通用架构,每次都要重新进行设计和实现,却无法在项目期限内获得高效、高质量的代码。
以上等等诸多问题,使得开发人员经常加班却看不到多少效果,使得软件开发人员对软件开发逐渐失去信心,造成恶性循环。
我从编码、设计、架构三个层次,将开发过程中的一些经验进行总结,形成了“轻松进行软件开发”三部曲,合理使用后能很轻松、高效地开发出高质量的软件。
本文是三部曲的第一部――编码篇。
编写0 Bug的代码,是每个程序开发人员的梦想,但这需要相关人员有很强的能力和经验,并在分析、设计、编码和测试阶段投入大量的时间、精力,但由于种种原因,效果往往不是很好。
本文介绍了一种低投入、高回报的编码技巧,可以帮助C/C++程序人员减少编码阶段的Bug,争取实现整个程序的0Bug。
限制
本文有如下限制:
1. 只适用于C/C++等通过函数返回值判断调用是否正确的语言(可能有些别的语言也适合),不适用于Java/C#这种靠异常进行错误处理的语言;
2. 只适用于编码阶段,不涉及分析和设计阶段。
原理
目前各种语言中常用的错误处理机制有以下几种:
1. 函数返回错误值 (如 COM返回的 HRESULT 和 Linux下的 pThread 函数族 );
2. 函数为表示错误而设定flag值 (如 Windows 的SetLastError 和 Linux 的 errno );
3. 抛出异常(Java 和 C#)
对于前两种错误机制的处理上,编码人员一般有两种做法:
1. 对大部分的返回值不予判断,认为程序的运行不会出现那些错误。虽然代码清爽了,但实际运行环境下当程序中出现函数调用失败时,由于没有及时处理,就留下了Bug隐患,直到N久之后才发作。于是程序员就需要花费大量的时间、精力去再现、确认、更改Bug;
2. 对所有函数调用的地方都进行判断和处理。于是代码中出现大量的if…else等分支判断,造成程序的编写、维护工作量大幅上升,但是很多代码估计永远都不会执行(谁能告诉我正常情况下CloseHandle什么时候会失败,失败后又该做什么?),而且往往在函数调用失败后,不判断具体的错误信息,只是简单的进行返回。
以上无论哪一种方法,都给开发人员增加了不少工作负担,而且代码出现了Bug也很难立即发现。因此,我通过设计、实现出CSTD(Code Self Test Development) 技术,自动对函数的返回结果进行检查,发现任何不对的地方,立即将错误的相关信息输出(日志和断言等),使得程序员可以专注于应用程序的逻辑,但又不会忽略任何可能的错误。
方法
以WindowsAPI的错误处理为例,介绍本方法。通常的错误处理逻辑为:
1. 调用函数(如CreateFile),判断返回值(如是否等于 INVALID_HANDLE_VALUE)
2. 如果有错误发生,通过 GetLastError 函数获取详细的错误码数值( 如 2 )
3. 通过 Error Lookup 工具或FormatMessage 函数获取错误的详细信息(如ERROR_FILE_NOT_FOUND)
4. 分析具体的错误类型,决定错误处理机制。
将以上步骤通过简单的XXX_VERIFY 宏进行包装,保证用最少的代码量完成函数调用、错误检查、日志输出、断言提示等功能。其宏定义如下:
#ifdef FTL_DEBUG
# define API_VERIFY(x) \
bRet = (x);\
if(FALSE ==bRet)\
{\
DWORD dwLastError = GetLastError();\
REPORT_ERROR_INFO(FTL::CFAPIErrorInfo, dwLastError,x);\
SetLastError(dwLastError);\
}
#else
# define API_VERIFY(x) \
bRet= (x);
#endif
详细分析和介绍
1. 通过自定义的宏(本处是 FTL_DEBUG)将错误处理机制区分为“错误发现”和“正常运行”版本,之所以不选择系统定义的 DEBUG/_DEBUG,是为了在Release版本下也可以启用错误发现机制。无论哪种版本,都会执行 (x) 指定的代码,并将返回值赋给bRet变量。
2. 在“错误发现”版本下,如果发现错误(返回值为FALSE),就通过GetLastError获取错误码,通过 CFAPIErrorInfo 类获取对用户友好的详细信息,并由REPORT_ERROR_INFO[h1] 宏输出日志和断言(此处的宏没有采用 TRACE 和 ASSERT 等系统自带的,目的也是为了在Release版本下能启用错误发现机制)
3. 为了防止错误发现机制对LastError的影响,在处理结束前使用SetLastError 恢复原有的错误码,保证之后的处理逻辑正确。
注意
1. XXX_VERIFY 只是帮助发现和更改错误的辅助机制,绝对不是错误处理逻辑。在发生错误时,一定要根据错误码进行后续的错误处理。对于大多数正常情况下不会、不该出错的代码(如 CloseHandle),可以简单使用 XXX_VERIFY 即可,但对于可能出错的代码(如 CreateFile ),可以参考例子程序中的用法。
2. 本方式有一个“副作用”,即必须存在特定名字的变量(如 bRet ),而且每次调用后都会更改这个变量的值。通常情况下,这是我们需要的结果。但是有的时候,会覆盖掉我们需要的结果。比如 CreateFile 成功,但后续处理失败时,需要返回对应错误码,并关闭文件Handle,如果对CloseHandle进行XXX_VERIFY,则会覆盖需要返回的 bRet 变量。这时一般有两种方法:使用其他的变量保存需要的返回码 或 只用 VERIFY 等宏对CloseHandle进行执行和断言。
3. Java语言使用异常机制,在编译时通过编译错误来保证对可能发生的错误进行处理;而XXX_VERIFY使用断言机制,在运行时通过断言错误来保证对可能发生的错误进行处理[f2] 。因此可见,XXX_VERIFY 的机制比Java语言提供的机制有很大差距,但已经可以尽量发现代码中的错误了。
示例
通过编写测试用的示例程序,详细介绍 XXX_VERIFY 宏的使用方法和注意事项
扩展
1. 通常来说,合理使用XXX_VERIFY宏,能在很少投入的情况下(只需将原有代码中的“函数调用”换成“XXX_VERIFY(函数调用)”)发现和解决大部分的编码Bug,但如果结合敏捷开发中的TDD,将发挥更大威力。使用UT搭建自动化运行的框架并对功能进行测试,代码内部通过 XXX_VERIFY 进行测试。在分析、设计时仔细考虑一下,加上开发人员的责任心(实际上这才是实现0Bug程序的根本),再通过这两个工具的结合,实现出 0 Bug的程序将不再是梦想[f3] 。
2. 推荐大家阅读一下 John Robbins 所编写的 《Microsoft .NET和Windows应用程序调试》,其中提到的“防御性编程”对大家高效开发0Bug的代码很有帮助。大家会发现我这里的 API_VERIFY 和其中的 SUPERASSERT 大同小异,可说是英雄所见略同(自夸一下J )。