#开发流程
一个完整的开发流程应该有这四步:分析->设计->编码->测试。很多开发团队往往只有编码这边,弱化了其他步骤,他们拿到需求就开始写代码, 写着写着发现有问题,要么是遇到一个难点解决不了,要么是发现要返回修改以前写过的代码, 要么是发现有大量的重复代码,又不知道怎么封装,只能将错就错。做好了分析和设计编码时就不会有这么多问题, 做好了测试产品bug就少,产品质量才高。 下面我分别详细讲解一下这四步。
## 分析
分析的时候,我们要分析需求和难点。
分析需求的方法是做需求陈述处理,前面我提到过, 要区分做什么和怎么做,把这两部分独立出来,做什么是固定不变的, 而怎么做可能会经常变。我们再熟悉一下举的那个例子:我们要做一个成员列表(如图1-44),产品经理告诉我们要按姓名拼音排序。
![](https://box.kancloud.cn/2016-01-12_56951562899d3.png)
图1-44 成员列表的例子
我们有时候不能直接听产品经理的,如果真写死成按姓名拼音排序就没有可扩展性了,比如某一天产品经理又告诉你需要把VIP会员提前,那么你只能再去修改排序的程序。这个需求始终不变的是排序,按姓名拼音只是排序的一种方法,我们在设计数据库时应该把排序字段设置为数字而不是拼音,再写一个拼音转换为数字的算法即可,这样在后面排序规则变化,比如VIP会员要提前,只是修改对应用户数据库的排序字段数值即可,不用大改程序。
我们可以用xmind做需求分析, 先把看见和听见的所有需求一条一条的列出来。拿到产品原型的时候,一点一点看,把看见的每个地方都先列出来 。 然后在每条需求进行分析, 看看能否区分做什么和怎么做,如果能区分,在做这条需求后面建两个分支,把分析结果写上去,如图1-45。
![](https://box.kancloud.cn/2016-01-12_5695156434354.png)
图1-45 用xmind做分析
需求过了一遍后,我们需要思考程序哪儿可能有难点,再把难点列到xmind中,比如程序列表这个功能,难点可能有:1,如何把中文姓名转换为拼音。2,如果把拼音转换为数字。
然后再想这两个难点的解决方案, 在网上搜索发现有中文转换拼音的开源库(overtrue/laravel-pinyin),我们直接用这个库就行,把解决方案写在xmind中对应难点的后面。另外,我们还应该做一个简单的demo,测试一些是否真的能解决。
程序员的二元性思维比较多,往往认为不是“是”就是“非”, 一个难点不能完美的解决感觉就不能解决,我们要有优雅降级的思维。假设,我们要做一个特殊的缓存的功能,缓存到服务端是最完美的,但有难度,不好实现。这时候想想能不能优雅降级,不能缓存到服务端能否缓存到客户端, 缓存到客户端方案不完美,比如用户可能会手动清空缓存,但是有缓存总比没有好,那么我们能不能做呢?
我们再分析刚才第二个难点, 拼音转换为数字的算法, 我们可能定义a转换为1, b转换为2,c为3。 但有一个问题,我们对人名的第一个字排序了,要不要对第二个字排序。 比如“张三”和“张飞”两个人名, 第一张的拼音首字母都是Z ,我们还需要对“三”和“飞”转换为拼音排序不? 要做完美的解决方案的话是要对第二个字进行排序的, 那么怎么排序? 假设我们在这里卡住了,暂时想不到好的解决方案,很多人就觉得这个功能做不了,不能做,我们能不能优雅降级一下,先只对第一个字最排序,以后想到其他解决方案再完善这一块呢?
## 设计
设计这一步,要体现出程序怎么写,我们要设计出数据库的表结构、API接口以及前端页面等。设计这步也可以在xmind中完成(如图1-46)。
![](https://box.kancloud.cn/2016-01-12_569515644fb85.png)
图1-46 用xmind做设计
设计这部分一定要体现出程序怎么开发,不能还是列举需求,要说明要建立什么程序文件、要建立什么类,以及类有哪些方法, 方法的具体处理过程,在xmind中经常会用这些短句: “当XXX条件时做XXX”,“调用XXX方法”。
xmind在列这些“处理过程”时,就能发现很多重复的逻辑,我们应该把这些重复的逻辑独立成模块(封装为类或函数)。这些要封装的模块我们在列xmind的时候就都想好了,而不是边写代码边想,这样会不断的推翻以前的代码再重写,更加浪费时间。
做设计这步时,在设计类的时候注意前面说过的“正交设计、类要有专职,善于用委托”。类方法的处理过程要详细到在实际写代码的时候照着xmind实现代码即可,那个时候不用想程序逻辑了。
对于新手来说,写代码之前把所有细节都想到是有难度的,他们要边写边想,写到具体的地方才知道有什么细节。但这样的习惯很不好,写着写着代码逻辑就会变乱。一定要慢慢养成写之前想清楚的习惯。从现在开始我们养成写代码之前列xmind做分析和设计的习惯, 写完代码后再对比之前列的xmind 看看哪些是之前没有想到的,要慢慢积累经验,时间长了分析和设计能做得越来越好。
我们在xmind中设计出有哪些接口和模块后,还利于团队的沟通和工作的分工。
xmind列好后开发人员要集体开会,所有人要理解某个模块怎么写程序, 并做好分工,评估每个模块的开发时间, 因为xmind列的比较细,某个模块的时间往往能以分钟或小时来估计,如有些模块还必须以天为单位估时间的话,那证明这个模块还能细化。
我们分工好后,可以在trello看板上做时间排期, 看板的用法后面会详细讲解。
## 编码
在编码阶段按照之前设计好的xmind编码, 编程时还要防卫式的编程,这样以后系统出问题我们能及时发现和修复。 下面详细介绍一下防卫式编程。
有时候我们对自己写的代码很自信,认为“这绝不会发生...”,比如我们读取某条数据,认为这条数据肯定不会为空,然后没有加任何判断代码。 如果某种之前没有考虑到的情况下,这条数据为空了,你的程序就有bug了。这时候你再一点一点慢慢去找问题,可能要花几个小时才能找到原因,而如果之前做好了防卫式编程,数据为空时有报警,就能让你马上解决问题。 防卫式编程就是在你认为不可能发生的地方加上判断代码,即使情况发生了我们也知道。比如
```
//读取一条用户数据
$user=M('User')->find($id);
//如果你认为这条用户数据肯定不为空,那么做个判断
if(empty($user)){
//如果为空了,发个系统报警,让开发人员知道。
warn('读取用户信息为空,用户uid:'.$id);
}
```
做好防卫式编程后,用户再向我们反馈bug,我们的第一反应是去看报警,一看报警就知道了,原来在某个情况下,读取用户信息可能为空。
上面示例代码中`warn`函数为自己自定义的一个报警函数,我们可以把报警信息发到手机短信或邮箱。
我们在“解决问题的方法”这一节讲到过有时候在找程序bug的时候,可能找的报错信息不是真正bug的原因,之所以出现这种情况也是没有做好防卫式编程,一个变量在赋值时就有问题了,那时没有做判断,在使用时程序才报错,问题原因不是变量使用时的问题,而是赋值时的问题。这好比飞机遭到恐怖袭击爆炸了,不一定是飞机的问题,可能是安检的问题。
另外对于程序可能出现的性能问题也要做好判断。
在产品初期用户量不大的时候,我们追加的是简单快速实现功能,然后上线收集用户反馈,再完善调整产品。如果一开始就程序设计得很好, 考虑高并发情况, 很可能上线产品没有高并发情况,甚至有因为用户反馈不好功能要做调整,导致之前写的代码作废。 所以一般产品初期时以简单快速实现功能为主, 产品后期才考虑性能问题。 但我们要对简单快速实现的代码加一个判断,让我们知道什么时候应该重构代码了。 举一个例子,比如我们给系统用户发邮件,刚开始用户不多, 我们可以foreach循环来发邮件。 当用户变多了,我们一定要知道这个地方的代码需要重构了,这时候需要把foreach循环的代码改为用队列发邮件了。
```
//读取所有用户的邮箱
$users=M('user')->field('email')->select();
//判断用户比较多的时候报警通知开发人员
if(count($users)>5000){
warn('用户数已经大于5000,需要重构发邮件的代码了');
}
//foreach循环发邮件
foreach($users as $user){
send_mail($user['email'],'邮件标题','邮件内容');
}
```
## 测试
测试的目的是为了减少程序的bug,自己写的程序自己一定要测试好了,确定没有问题再交付给其他同事。 有的人工作经验越久就越来越自信,认为自己写的代码一定没有问题,慢慢养成了写代码不测试的习惯。这样会影响团队之间协作,在团队其他成员那里印象也不好。
想想我们如果是这样的一个工作方式: 后端告诉前端接口写好了,前端十分惊讶觉得后端的工作效率好高呀, 自己要加快速度了,前端的程序还没有写到要调接口的地方。后端此时无所事事悠闲的听着音乐。前端终于程序写到调用接口的地方了,结果一调用接口发现接口还报程序语法错误,前端十分生气但是还是压住火告诉后端接口有问题。 后端回答“哦,是吗?我看看” ,后端去查找程序问题时,前端真的没事干了,只能处于等待状态。 过一会儿后端告诉前端 “程序好了,可以了” ,前端再调用接口没有语法错误了,但发现逻辑走不通, 前端终于怒了,大声对后端说 “你写程序能不能自己测试一下” 。 后端理直气壮的回答“你反正都要调接口,不就相当于给我做测试了?”
这样的工作方式效率是极其低的,后端是把本属于自己的测试工作想让前端测,后端在修改接口的时候前端只能等待,浪费前端的时间,前端也会很烦这样的后端,不愿意和他合作。很多时候程序员和产品经理的合作也是这样,程序员告诉产品经理程序写好了,而产品经理测试有很多问题,程序员和产品经理的矛盾就此产生了。甚至如果没有人给程序员把关,直接把产品呈现给了用户, 用户操作一两步发现有问题就直接人走,他才没有这么好心帮你测试,给你反馈问题。
做好测试,减少程序bug有下面四种方式:
* 1,人工测试
人工测试是最简单的方法,自己写的程序一定要自己测试,而且要早测试,程序写多了再测试,遇到bug找程序问题可能不太好找。
人工测试至少要保证自己写的程序没有语法错误和逻辑性错误。
如果交付的程序都有语法错误,那肯定是没有做测试的。
根据产品需求还要验证逻辑是不是对的,有时候一个逻辑涉及到好几处功能,这几处都要连起来测试一下。
* 2,单元测试
我们在做人工测试的时候经常需要准备一些测试数据。比如:提交表单时填写的数据、请求接口时的请求参数。下次再要测试这个功能又要重新填写这些数据。“自动化”是程序员的生产力。人工测试也是能用测试自动化的, 我们可以写“测试程序”来测试程序的功能,这样那些每次都要填写的数据,在测试程序里面只写一次可以重复利用。下次要测试同一个功能,只要运行测试程序即可。很多人认为写自动化测试程序很浪费时间,其实我们每次人工测试填写测试数据同样浪费时间,何不一劳永逸呢?
自动化程序测试又分好几类, 有单元测试,集成测试,黑盒测试,端对端测试等等。
单元测试是人们提得最多的。各个编程语言都有单元测试框架, 如PHP有phpunit 、Java有junit 、 iOS有XCTest,而且iOS在创建项目时就有默认的单元测试代码,足见苹果对单元测试的重视。 大家要掌握一种你用的程序语言的单元测试框架。
单元是指一个不可在分的模块,比如一个函数,一个类。某个功能可以由很多单元组成,它要调用多个函数或类。 而单元测试是要求我们从小单元开始测试,写程序去测试函数或类,判断函数或类的返回值是否正确。
写好单元测试能强制让我们把程序架构设计好,高耦合的程序是无法写单元测试的。我们必须做到程序的低耦合,一个函数不会和很多函数有关系,才好做单元测试。
写好单元测试也能帮助以后重构和修改代码。我们往往修改一次代码可能会影响其他多处代码,容易产生bug,靠人工测试很容易漏测, 而如果有单元测试,我们只要跑一下单元测试程序就知道自己修改的代码有没有问题。
人们提倡TDD开发模式很多年了, TDD(Test Driven Development)测试驱动开发,也就是说开发代码之前先写测试代码。但这是要正真做到TDD开发还是有难度,我们往往为了赶项目进度,程序员不愿意写单元测试。能坚持写单元测试的团队不多, 在国内一个开发团队能写单元测试那么他们的开发能力一定是国内领先的。
* 3,做好报警
我们单元测试很难做到把所有地方都测试到,也就是单元测试的覆盖率很难到达100%。 人工测试也不敢保证把所有地方都测试到了。我们可以做好系统报警,这样即使用户触发了我没有测试到的bug,我们也能收到系统报警,然后及时修复bug。
当用户触发到程序bug的时候,程序往往会有报错,我们应该把程序报错做成系统报警然后通知开发人员。
程序报错一般分为两种,一种是FatalError终止性报错。一种是warning报错。
FatalError终止性报错是当程序出现严重错误(如语法错误)导致程序无法继续运行,必须终止程序
warnning报错 是程序认为出现了小错误了,但程序还能继续运行,不道德但没有违法,不会被抓起来。 举两个PHP的例子:用`file_get_contents` 读取文件,如果文件不存在会报warning错误,但不终止程序,会把文件内容当作为空字符串处理,继续执行下面的程序; 另一个例子:在使用不存在的数组下标时,也会报warning错误。 有的人认为warning报错不重要,反正程序能正常运行,甚至把warning报错屏蔽掉。但warnning报错往往能反映程序有bug,比如使用了一个不存在的数组下标,可能是因为粗心把下标的单词写错了,但如果屏蔽了warning报错,这个粗心导致的bug很难被发现。
触发程序报错的有可能不是开发人员,而是用户,用户看不懂程序报错,需要把报错做成报警通知给开发者。 我以PHP为例,PHP可以用set_error_handler和register_shutdown_function来接管报错,如:
```
<?php
set_error_handler('error_handler');
register_shutdown_function('fatalError');
error_handler($errno, $errstr, $errfile, $errline){
switch($errno){
case E_WARNING: $severity = 'E_WARNING'; break;
case E_NOTICE: $severity = 'E_NOTICE'; break;
case E_USER_ERROR: $severity = 'E_USER_ERROR'; break;
case E_USER_WARNING: $severity = 'E_USER_WARNING'; break;
case E_USER_NOTICE: $severity = 'E_USER_NOTICE'; break;
case E_STRICT: $severity = 'E_STRICT'; break;
case E_RECOVERABLE_ERROR: $severity = 'E_RECOVERABLE_ERROR'; break;
case E_DEPRECATED: $severity = 'E_DEPRECATED'; break;
case E_USER_DEPRECATED: $severity = 'E_USER_DEPRECATED'; break;
case E_ERROR: $severity = 'E_ERR'; break;
case E_PARSE: $severity = 'E_PARSE'; break;
case E_CORE_ERROR: $severity = 'E_CORE_ERROR'; break;
case E_COMPILE_ERROR: $severity = 'E_COMPILE_ERROR'; break;
case E_USER_ERROR: $severity = 'E_USER_ERROR'; break;
default: $severity= 'E_UNKNOWN_ERROR_'.$errno; break;
}
$msg="{$severity}: {$errstr} in {$errfile} on line {$errline}";
warn($msg);//调用报警函数
}
function fatalError(){
if ($e = error_get_last())
{
error_handler($e['type'],$e['message'],$e['file'],$e['line']);
}
}
//报警函数
function warn($msg){
//获得调用栈
ob_start();
debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$trace = ob_get_contents();
ob_end_clean();
//格式化调用栈
$trace = preg_replace ('/^#0\s+' . __FUNCTION__ . "[^\n]*\n/", '', $trace, 1);
$trace = preg_replace ('/^#(\d+)/me', '\'<br />#\' . ($1 - 1)', $trace);
$msg.=$trace;//报警信息加上调用栈
send_mail('upfy@qq.com','系统报警',$msg);
}
```
上面代码set_error_handler函数告诉了程序如果有报错执行error_handler函数 , error_handler有四个参数, $errno 为错误码, $errstr 为错误信息, $errfile 为错误程序文件地址, $errline 错误所在文件的行数 。 $errno为整数可读性不好,所以代码中用switch判断整数的值设置一个可读性较好的字符串。 另外有debug_print_backtrace 获得了调用栈, 报警信息中有调用栈的话方便我们分析程序的问题。
因为set_error_handler函数不能接管终止性的错误信息,所以程序有用register_shutdown_function 指定程序终止时要执行的函数fatalError。 fatalError中用error_get_last获得错误信息并再调用error_handler函数。
上面的示例代码只是简单的举例,正式的报警系统的代码可能考虑的问题还有更多,简单说一下下面两个问题。
* 1,如何防止重复的报警信息
我们不能让报警系统重复的报警信息一直发给开发者,比如有一个程序bug,但现在同时5千人在访问这个程序,不能报警5千次吧。 我们可以把发过的报警信息缓存一段时间,发报警时查询一下缓存里面有没有一样的信息,相同信息就不要再发了,缓存可以用memcache等模块做, 缓存时间可以设置为5分钟。这样重复报警信息5分钟只会发一次。
* 2,如何跟踪触发报警的用户
很多用户遇到产品有bug就直接走人,他才不会好心给你反馈问题。我们如果还想知道报警是哪个用户触发的,可以在报警代码出读取一个用户信息一并写到报警信息中,比如可以读一下这个用户的用户名,手机号等有用信息。我们修复bug后还能联系之前触发了bug的用户让他回来继续使用产品。
现在除了邮件、短信等来接收报警信息以外, 还有更好的工具可以用slack , slack既有PC客户端也有手机客户端,这样发送的报警在电脑上能收到、在手机上也能收到。往slack上发送报警信息只要调用slack的接口即可,使用slack访问官网:http://slack.com 。但slack是国外软件,国内使用稍微比较慢,内国可以使用仿slack的产品:纷云(https://lesschat.com )。
* 4,CodeReview
CodeReview也是减少程序bug的一个手段,CodeReview是指团队成员之间互相审核代码,CodeReview能让我们发现程序因为粗心导致的低级问题或者代码性能的问题。有些问题是做人工测试发现不了的。
比如有人在for循环里面查询数据库,这是性能极其低的写法,只有新手才这么干,这样会导致每次访问程序都可能查询几十次数据库, 这几十次查询其实可以合并成一条用in查询的SQL语句,这样只一次查询就可以。而这种问题只从产品功能来看是看不出问题的,不看代码是发现不了。
CodeReview还能让团队每个人都了解整个程序,避免出现“这程序不是我写的,我改不了”的情况。
有的人很难静下心来看别人的代码,那证明还处于实现阶段还没有进入借鉴阶段,可以用本书前面说的分析代码的方法去看别人的代码。
如果我们开发前做好了分析和设计的xmind,那么看代码之前先看看xmind上面列的程序处理逻辑,这样再去看代码就比较容易了。
每次CodeReview看新增或修改的代码即可,以前看过的代码不用再看,不用每次都从头看起。可以用版本控制工具对比功能来做CodeReview ,SVN用命令:`svn diff` , GIT用命令:`git difftool`, 具体svn或git的用法大家在网上搜索更多资料。
以上四种方法能有效的保证项目的质量,我们要写好单元测试,但有很难做到单元测试覆盖率100%,要确保主要功能做了单元测试,有些功能无法用自动化程序测试的就人工测试,做好CodeReview可以发现人工测试发现不了的问题,还有做好报警系统,这样即使用户触发了我们没有测试到的bug,我们能及时知道并及时修复。
另外还想提醒大家:好的团队需要时间。不可强求团队马上就能把这些流程和工具用好。