SoftEng 版 (精华区)

发信人: Sun (大灯泡), 信区: SoftEng
标  题: [selab] 微软开发者之路 -- 如何编写产品级代码
发信站: 哈工大紫丁香 (2001年10月25日15:06:23 星期四), 站内信件

Rick Zhang <rickzhang@sina.com>  2001-10-25 11:02  [selab] 微软开发者之路 -- 
如何编写产品级代码[整理和心得]  selab

设计文档可靠的产品级代码



以上是我针对APEC期间, 微软在上海进行的一次"开发者成功之路"的各个讲座进行的整理
, 附加了一些个人心得.



[概要]

. 目标: 编写高质量的代码

- 正确对待Bug

- 游戏规则

- 编程技巧

- 资源和工具



. 对象

- 软件管理和开发人员





1. 正确对待Bug



1) 什么是Bug

影响客户正常使用产品的任何问题





2) 注意点

- 客户是上帝, 产品属于客户而不是开发者, 为此开发者需要本着对客户而不是老板负责
的态度

- Bug的来源可以贯串整个开发的各个环节

按照RUP的开发过程:



阶段 可能的Bug

------------------------------------------------------------------------------
-----------

项目开端 范围过大, 导致项目失控 -- 间接影响, 应该属于心理上的Bug, 要知道项目都
是建立在

开发商和客户的信任基础上的, 而目前的情况就是开发商或者市场部门忽视客户的存在

或者不愿意接触研究市场导致一厢情愿的试图开发一个"完整"的产品, 给客户提供需要

的所有功能, 但是往往事与愿违.



系统精化 1. 需求二义性导致错误的业务模型

2. 错误的业务模型导致不完整的不正确的分析和设计

3. 过分乐观导致风险意识淡泊, 对于系统潜在的风险(需求, 技术, 技能, 政策)估计不


足, 进而导致项目范围, 进度, 资源配置失控, 最终挫伤开发人员积极性, 导致极大

的客户产品期望差异值, 乃至严重到引起公司人员流失和法律纠纷



系统构建 规模, 方向, 复杂等方面的错误导致项目开发难度提高



系统提交 上述因素引起的期望差异值引起产品化困难, 返工重重

------------------------------------------------------------------------------
-----------





3) 为什么关注Bug

- 降低产品质量

- 项目开发失控(日期, 人力物力配置)

- 损害团队积极性

- 损害未来产品的版本

- 降低工作效率



解决方法

- 沟通, 不但是开发团队内部沟通, 还需要和客户在整个项目过程中随时沟通, 因为就软
件而言, 不但包括开发商还包括客户, 毕竟软件产品是属于客户的

- 责权, 开发商内部各个部门责权分明, 发挥在项目之中应由的作用; 外部和客户之间明
确权利义务, 建立信任基础上的项目分工





4) Bug的来源

- 项目期限的压力

常常导致管理层忽略过程过分追求结果, 由此上行下效导致开发团队内心对于产品保质失
去信心和耐心敷衍了事, 产生厌战情绪

- 产品复杂度

在没有经过充分分析, 设计和风险评估之前, 盲目乐观自信往往造成很多开发过程之中的
不可预计的失误

- 开发人员的疲劳, 压力和受到干扰

在国内没有人真正尊重300行/人月的国际开发进度参考, 造成往往一个产品推出来, 一批
Bug和人员倒下去

- 缺乏足够的知识, 技能和经验

管理层往往过高估计开发人员的学习能力和藐视学习曲线的客观规律导致"以赛带练", 最
终锻炼出一批有经验的未来竞争对手麾下的程序员

- 不了解客户需求

"面向管理层, 背对客户", 这是很多软件公司过分书生气的表现

- 缺乏动力

很多软件公司仅仅存在技术组合, 在管理和营销上都存在种种弊端



其实, 面对Bug就好像我以前说过的面对风险, 应该采取主动评估, 量力而为的心态应对




5) 怎样减少Bug

- 开发中为质量工作留足时间

其实, 按照倒金字塔原则, 开发多数集中在技术研究和分析设计上. 而且需要正视代码上
, 产品/测试的1:1原则, 即实现系统对应测试系统的平等比重

- 越简单越好

根据2/8原则, 真正给系统用户带来收益的往往就是20%的系统基本功能

按照微软的法则, 80%的Bug来自界面

根据抽象法则, 真正灵活和可进化的恰恰就是那些抓住系统主要功能特点因素抽象的基本
系统

- 重视和避免开发人员的疲劳, 压力或受到干扰

以人为本, 不要忽视项目开发本身就是一个学习过程, 极大程度上依赖开发人员的技术和
技能程度, 为此不要才用纯工业化流水线的思路, 疲劳战术更是要不得

"得道多助, 失道寡助", 为此希望锻炼员工, 又希望产品早日出炉, 更加希望公司飞速发
展. 这样的便宜事天下少有, 虽然原始积累往往是"血腥的", 但是输了人心, 你还能赢什


- 增加知识, 技能和经验

重视开发团队中"识途老马"的作用, 经历是一种财富, 发挥作用并非一定委以重任

培养学习风气, 真正的高科技公司就是懂得学习的公司

培训是公司的义务, 需要投入, 也是一种长期投资

培训是素质教育不是给机器加油那样立杆见影, 需要具备战略眼光(柳传志的话不无道理
)

- 了解你的客户

在整个商业环节之中客户既是起点也是终点和新的起点, 项目以客户利益需求为第一原动
力, 技术只是必要条件

- 把项目质量和个人利益挂钩

其实就是确认现代企业的新的雇佣关系 -- 合作, 同舟共济, 同仇敌忾

- 树立正确的Bug观念

失败意味着将来的成功

完美的事物只是梦境中才有, 但是无限接近就是一种态度



2. 游戏规则

1) 在程序设计上倾注心力

[完善的功能规范]

. 功能规范

- 站在用户的立场考虑问题 -- 业务策略和目标

例子: 

客户为什么需要开发系统 ? 

答: 管理层指令, 旧系统升级, 业务模式转换压力等等...

系统可以为客户带来多少收益 ?

答: 以货币单位计量的改变, 比如节省20%的开支, 提高30%的工作效率...

系统用户描述的业务流程, 特别是如何优化流程提高并发性, 并且从中获得操作角色

答: 这就是USE CASE的方式

系统受那些制约

答: 比如政府法规, 行业规范...

- 不需要描述实现方法

既不要将自己的设计带进规范, 也不要受客户的影响, 你的目的就是在挖掘客户需求和客
户取得一致性的认识以确定项目范围和基本业务功能以及相关的限制

. 评审功能规范

- 用例方式

尽可能列举出所有的操作用例, 我们开发系统就好像指挥航船, 不能使他偏离轨道, 直至
抵达彼岸(收钱喽....呸, 俗!!!)

- 发现并解决问题

参与这可以包括客户核心人物, 专家和分析人员和公司管理层, 从各个角度查询相关错误


- 越具体越好

实现开始时间越晚, 对于项目越是有利. 本阶段的工作中心就是获得详细准确可以得到客
户认同的需求功能规范

- 写出初步的功能设计文档

其实应该遵照认识的基本规律由浅入深的进行





[功能设计文档的好处]

. 强迫你在动手编程之前找出解决难题

- 避免编程中的不确定因素

- 避免编程中忘记重要的客户要求

- 避免做无用工

- 加快编程速度

. 有助于制作进度表

- 推荐使用FPA等工业化算法

. 帮助其他开发人员为你提供建议和参考

. 为你的代码提供注释



[功能设计文档的内容]

. 描述数据结构和接口

. 发现并解决难题

. 链接所有不需要做的事情

- 将需求分类成为"必要", "非必要"和客户讨论不同版本完成的各自内容

. 使用检查清单或设计文档模板

- 其实项目过程常常就是伴随着过程讨论和文档模板这些指导性元素而进行的



[设计:拿来主义]

. 首先考虑借用现成代码

- 复用带来效率

节省时间, 提高质量

- 复用级别方式

可执行文件(Dlls, Libraries), 源程序, 算法, 构架, 用户界面

不同领域(基础, 构架, 商业, 应用)的复用级别不同, 为此真正需要复用的绝对不是产品
, 这是非常危险的看法, 因为应用领域的复用级别是最低的, 不要试图为了解释占据整个
项目时间15%的编码时间而做错误的决定

. 复用代码的来源

- 当前或者正在开发的项目

客户有权要求开发商利用已经有的组件

- 系统调用

- 标准代码库

- 其他项目

. 可能带来的问题(复用风险)

- 需要时间集成

- 代码质量低下

- 代码不合要求

- 给开发带来不确定因素



[设计:编写开发测试计划]

. 编程之前编写测试计划

- 编程时会考虑到的测试要求

- 测试要求不随编程而改变

- 可以避免敷衍测试的行为

. 测试计划需要考虑

- 用户情景

- 输入/输出

- 边界情况

- 可能出现的错误

- 功能之间的集成和连接

* 一般就是单元测试和集成测试, 具体方法存在黑盒法和白盒法, 为此给每一个代码单元
添加断点或者添加调试代码(1:1)是非常有效的测试调试习惯



2) 照顾到代码的方方面面

[好代码:简洁]

. 设计清晰, 无误解的接口和对象

. 使用简单的算法和流程

- 避免模糊, 隐藏, 难懂, 好玩和偷懒

- 把过长和身兼多职的函数细分(其实就是考虑如何降低耦合性, 最好是数据型而不是功
能型, 不过有时候回调函数需要如此)

. 不要为改善性能而盲目改动程序

- 先做性能测试

- 对症下药: 只修改会影响性能的部分

. 避免编写不必要的代码



[好代码:可读]

. 让注释有用并容易维持

. 使用统一的编程标准

- 有助于改善程序之间的沟通

- 定义编程标准文档

. 包含命名, 格式, 以及需要遵守和避免的原则

. 采用标准比标准如何定义更重要



[好代码:一丝不苟]

. 处理所有出错情况

. 检查所有返回值

. 验证所有外部参数

. 吃透你要实现的函数调用

- 你的假设是否正确

- 杜绝"走一步看一步"

- 尽可能多了解你的功能和项目

. 客户和功能规范

. 架构和代码设计

. 开发环境(团队, 规范, 软硬件)



[好代码:双程序员合作制度]

. 好处

- 集中精力, 提高质量

- 互相验证对方的假设

- 木笔对方考虑的盲点

- 提供Code Review

- 对现有代码特别有效

. 现状

目前国内的情况就是, 很多老板将"多余"的配置视为额外负担, 常常是一个关键人物承担
了大量工作, 一旦人员流失常常造成整个项目停滞, 有时候真是感到我们的观念是否用错
了地方, 将开发过程流水线化违背认识客观规律, 对于编码也是如此, 长此以往无非是为
那些正规软件公司做"突击培训"



[好代码:编程过程中的测试]

. 在调试环境下走过所有代码(白盒法)

- 为所有代码模块设置断点

- 到达断点并验证代码块以后, 清除断点

. 立即对每个功能的可测试部分做测试

. 设计和实现中都要考虑可测试性

- 保障每一个代码路径都会被执行

- 通常需要使用调试专用代码



[好代码:调试专用代码和工具]

. 调试专用代码

- 专为找Bug写的代码

- 与其它代码一起设计和实现

. 工具

- 帮助你发现Bug的工具



[没有及早修正Bug的代价]

. 程序员额外工作

. 测试员额外工作

. 整个项目测试停顿

. Bug此起彼伏

. 修正代码影响其它程序员工作



[良好的编码整理习惯: Daily Build]

. 每天构造最新代码

. 自动进行每天构造

. 及时集成所有模块

. 及时发现Bug

* BVT( Build Verfify Test)



[代码审阅]

. 技术角度

- 及时发现问题

- 其它程序员熟悉代码

- 训练新程序员的最佳方法

. 规定专门的代码审阅时间

. 审阅代码技巧

- 适量代码审阅(不超过1000行)

- 把代码连同行号打印出来

- 分配角色

* 代码负责人: 事先单独审阅代码, 事后改正发现的问题

* 代码审阅人: 审阅代码, 发现问题

* 观察员 : 学习或提供经验

- 审阅代码会

提出, 讨论并解决问题



[修正Bug的地位]

. Bug优先于新功能

. 原因:

- 越晚改正代价越高

- 产品稳定性有保障

- 容易分清程序员的优劣

* 好的程序员尽快完成任务

* 不注意质量的程序员只能原地踏步



[现实情况]

. 不是所有的项目经理都认识到质量重于进度

. 期限的压力

- 在进度计划中为设计, 测试和审阅留足时间

. 需要团队配合

- 所有成员需要有质量观念

- 建立团队的规则保证质量

* Bug优先级

- 系统崩溃

- 可重现

- 界面等非必要情况





3. 编程技巧

1) 调试专用代码和断言

-==调试专用代码==-

[做法]

. 专为发现bug而写

. 只在_DEBUG构造中执行

[原则]

. Retail构造必须不受影响

. debug构造可以背Retail构造慢

[例子]

void *memcpy( void *pvTo, void *pvFrom size_t size )

{

byte *pbTo = (byte*)pvTo;

byte *pbFrom = (byte*)pvFrom;



#ifdef _DEBUG

if ( ( pvTo==NULL) || (pvFrom==NULL ) ){

fprintf(stderr, "Bad arg to memcpyn" );

abort();

}

#endif

while ( size-- > 0 )

*pbTo++ = *pbFrom++;

return pvTo;

}



-==断言==-

[作用]

. 一个宏, 在表达式FALSE时停止程序执行

. 用于表达和验证代码中的假设

. 只在_DEBUG构造中执行

. 在Bug尚未从功能上暴露时就被发现

. 报告出现Bug的文件名和行号

. 易写和维护

. 支持运行中切换到调试模式

[使用]

. 仅仅用于检验Bug

- 非验证程序本应处理的异常

- 不要隐藏Bug

. 检验对象

- 数据正确性: 参数, 变量, 对象

- 算法正确性: 前提条件, 中间状态, 后续条件

- 应当出现的状态

- 你做的任何假设

[实例]

//普通例子

void *memcpy( void *pvTo, void *pvFrom size_t size )

{

byte *pbTo = (byte*)pvTo;

byte *pbFrom = (byte*)pvFrom;



ASSERT( pvTo );

ASSERT( pvFrom );



while ( size-- > 0 )

*pbTo++ = *pbFrom++;

return pvTo;

}



//参数范围检验

void SetObjectType( OBJ *pobj, OBJTYPE ot )

{

ASSERT( pobj!=NULL );

ASSERT( FObjectIsValid(pobj) );

ASSERT( ot!=otNil );

ASSERT( ot>=otMin );

ASSERT( ot<=otMax );

...

}



//用断言检验返回值

OBJECT *pobj=PobjGet();

...

if (FEnableObject(pobj)){

ASSERT( FObjectIsEnabled(pobj) );

UseObject( pobj );

}

else{

ASSERT( !FObjectIsEnabled(pobj));

ReleaseObject(pobj);





//检验预期外的状态 

void *memcpy( void *pvTo, void *pvFrom size_t size )

{

byte *pbTo = (byte*)pvTo;

byte *pbFrom = (byte*)pvFrom;



ASSERT( pvTo );

ASSERT( pvFrom );



//预测外行为

ASSERT( (pbTo>=pbFrom + size)||(pbFrom>=pbTo + size) );



while ( size-- > 0 )

*pbTo++ = *pbFrom++;

return pvTo;

}



2) 数据校验

[方法]

. 配置调试专用代码

. 每一个对象都必须具备验证成员有效性的Validate()方法

- 检验每一个成员

- 使用对象前后调用

. 全局检验函数ValidateAll()

- 调用所有未释放对象的Validate()函数

- 检查全局的属性和对象间的关系

- 重要操作前后调用ValidateAll(): 例如启动和退出

[数据校验模板]

struct FOO foo;

CBar bar;

...

VidateFoo(&foo);

bar.Validate();

...

ValidateAllFoo();

CBar::ValidateAll();



3) 算法验证

[方法]

. 检查算法(或流程)达到预期目的

. 使用断言检查前提条件, 中间状态, 后续条件

. 使用其它简单算法对比结果

[算法校验模板]

//前提

#ifdef _DEBUG

...

#endif



//中间状态

...执行算法...



//后置条件

#ifdef _DEBUG

...

#endif



4) 预防某些类型的Bug

[数据结构/函数调用/接口]

. 别人使用我的数据结构/函数调用/接口时会出现什么问题

. 使用调试专用代码检验每种情况

. 情况: realloc()有可能移动对象的内存地址

. 可能的bug: 代码可能假定对象地址不变

. 解决办法: 使用调试专用的realloc()

- 每次移动对象的地址

- 让旧地址无效



[分配内存]

. 情况: 动态分配内存

. 可能出现的Bug: 有的域没有初始化

. 解决方法: 使用专门的调试代码实现内存分配

- 用错误值初始化所有域

* 这些值将无法通过Validate()检验

* 使用无效指针(0xcdcdcdcd)填充内存块

. 可能出现的bug: 使用被释放的内存

. 解决方法: 释放对象时打乱它的内存, 使得无法被使用



5) 代码的可测试程度

[目标]

. 每一个路径都能够被检验

[方法]

. 作为设计的目标之一

. 测试接口: 测试不依赖于用户界面

. 在构造中放入测试数据和加载方法

. 模拟难以出现或者难以重现的情况

- 确保检验不易到达的代码

- 模拟函数调用返回错误

* 内存不足

* 缺少文件访问权限

* COM访问失败

- 设置专门的超负荷构造(STREESS Build)增加数据量和其它负荷

. 实现调试菜单

. 保护内存分配

- 分配内存时用额外内存:

* 已分配内存对象清单

* 每一个对象是何时, 何处创建的

* 每一个内存块加入缓存区以检测内存块以外地址的访问

- 用以验证指针和heap的函数调用

- 用以发现指针和heap的函数调用

- 用以发现和清除流失内存的函数调用

- 实现方式

* C Runtime

* 工具: BoundsChecker




______________________________________

我的QQ : 329170

我的ICQ : 37175293

我的论坛: http://www3.ccw.com.cn c/c++
===================================================================
新浪免费电子邮箱 ( http://mail.sina.com.cn )
新浪分类信息--城市生活指南! ( http://classad.sina.com.cn/ )
世界杯、甲A、欧洲联赛…订足球短信,赢手机大奖! ( http://sms.sina.com.
cn/sport.html )


++++++ 诚邀加盟 SE Forum Team +++++
本消息来自SE Forum China: http://www.seforum.net 
退订请发信到: selab-unsubscribe@yahoogroups.com 


使用 Yahoo! 电子部落 请遵守 http://cn.docs.yahoo.com/info/utos.html 


--
    太阳当空照,灯泡呵呵笑,
    mm说,早上好,你为什么又不理我了?
    我已脱光了,mm管得牢,
    高高跳,大声叫,幸福的生活需要我们共同来创造!

※ 来源:·哈工大紫丁香 bbs.hit.edu.cn·[FROM: 172.16.8.66]
[百宝箱] [返回首页] [上级目录] [根目录] [返回顶部] [刷新] [返回]
Powered by KBS BBS 2.0 (http://dev.kcn.cn)
页面执行时间:212.171毫秒