写作前唠叨一会 |
昨天下班,我刚从公司大门出来,突然感觉自己今天下班好像下早了。因为公司外面还挺热闹,有人摆摊卖小吃,对此我有点不习惯。当然还是要解释一下,公司并没有强制要求加班,只是我几乎每天下班都在办公室里呆得比较晚,直至天黑了才慢悠悠的出来。不过我并不认为自己有多努力,那是因为:有时工作累了,想留下来打几把游戏再回去。也有时因工作遇到技术难点,我不得不抽点时间去学习和查缺补漏。
由于下班的挺早,太阳尽管很热,我的内心仍就感到舒坦。自己从兜里拿了一块同事给的狗屎糖,细嚼慢咽,品尝着“狗屎样”的甜味。
在回去的路上,一直在思考着过去工作的点点滴滴。在这些年来,我做过很多苦恼的编码之事。还记得那个陪伴我四年之久的项目,我曾为此已经付出了很大的努力,可结果还是挽救不了它。我亲眼见证了这个产品的成长之路(同时它也是我成长的小影子),它如同稚嫩的孩童成长为行动缓慢的老头。我曾为它注入了新血液,结果又给我漏了出来。那种感受,我有点无语。后来我离开了,听说公司已经在为它进行重构,我感到欣慰。
直至我又加入新的团队,再经历了一段摸着石头过河的艰难过程。当自己不断地优化代码,重整结构以及想方设法挤出对一系列问题的解决方案,才慢慢发现自己也正在成长,深知解决产品的问题才能体现出程序员在工作上的价值。
其实我并不是一名优秀的程序员,跟绝大部分码农一样,也会面临来自年龄和就业的压力。当然这些问题并不是本文主要讲的内容。只是作为有过多年工作经验的普通程序员,我更喜欢分享的是自己过去的那些编程经验,同时也希望有人能从文中发现并解答出我心中的一些潜在疑惑。
本文从产品需求、项目管理和编码设计这三个方面进行探讨,这三个方面都是围绕着程序员的角度来思考问题的。
一、产品需求
你面临的是什么样的需求 |
在谈需求之前,我先举个例子:
程序员李四下班了,而程序员张三在忙着加班,还在埋头嘀咕着产品经理的不对。
李四问张三:“老哥,昨天看你好好的,今天发现你有点不对?”
张三说:“那肯定啊,我还要留下来加班。产品经理都催了,说这个功能马上做好,明天要给他看看。”
李四:“老哥,请细说!”。
张三说:“功能都做好了,眼看交付期限也快到了,才说这不是他想要的效果。”
李四听后摇头,叹了口气“安慰”了一把张三,然后开心地回家了。
从例子中看出,张三面临的问题是“做出来的功能却不是产品经理想要的”。为什么会出现大家对需求的理解不一样呢?
很多时候,程序员把问题都归咎于产品经理的需求不明确造成的。在工作中,这种情况时常发生,尤其在项目管理不够完善,又是开发新产品的时候。
当产品经理提出需求时,只是简单的提出几点要素,就直接丢给程序员去做。所以程序员可能也会按照自己的那套产品思维来处理。在开发过程中,你不说,他也不说,中间也没有过多关于需求的沟通,结果就如同上面的例子那样,只能通过加班赶工来完成。
领导分配了任务给我们。我们在开始工作之前,先别急着动手编码,必须要了解自己手中的产品是什么类型的。有疑虑时,要时常保持沟通。
有工作经验的软件工程师每次变换不同业务方向时,他们都会有一套与之前不同的编码设计。因为他们知道自己面临的是什么样的产品,就必须设计出不一样的代码架构。如果产品是面向C端用户,迭代性强,考虑更多的是功能的扩展性。而产品是面向B端,考虑更多的可能是功能的维护性。如果是小项目,那么设计的代码结构就没必要过于抽象化。
其实最终目的还是要弄清楚,自己手动的产品到底面向的是哪个用户群体。
需求文档 |
在急急忙忙交付完项目后,我们却有这样的困惑:明明规划好的开发周期已足够,到上线之前还是要么砍掉功能,要么还存在很多 bug?明明看似很简单的功能,一旦着手去做了,还是要花大量精力进行优化重构?
带着这两个问题,我先在此举个假设例子:
领导说:“小陈,用户有一个重要的需求,想在程序里添加读写文件功能。”
好,小陈认为这个功能很简单。他很自信地向领导打包票,说一个小时就可以完成。起初他确实按时做到了,顺便还有时间喝上一杯奶茶,走过去跟测试工程师扯扯牛皮。测试工程师测试才进行了一半,就已经发现了很多 bug ,于是马上让小陈滚到工位上修改 bug。小陈二话不说,赶紧改了又交给测试,结果不但旧 bug 没修复好,还出现了新 bug。于是小陈折腾了两周才勉强交付了任务。
经过几次回归测试后,这个情况已经影响了项目推进的进度,并且这个功能的稳定性也很差。
为什么小陈明明把东西做好了,还是没法正常交付任务?从例子中大家都知道,那是因为他给自己开发的时间太短了,根本没有考虑那么多。读写文件虽然很简单,但是有很多潜在的技术点,不是简单地存取修改内存上的数据。
作为有经验的程序员,我们要考虑到读文件之前要注意什么,以及修改文件中要处理什么。
以下我们专为小陈在开发之前整理的需求文档:
《需求分析文档》示例
一、需求背景:
用户对文件进行处理,需要借助一款软件来完成。
二、需求目的:
读写文件功能属于必要需求。如果直接使用系统自带的文本编辑器的话,用户处理多个文件时,需要一个个打开又关闭,就显得很麻烦。于是需要有一个功能,可以自动批量处理文件。
三、技术分析:
读文件:
1、读取权限:如果要读取一个只读或者有密码权限的文件,就要告知用户需要写权限或者输入密码。
2、文件本身:如果用户读取的是一个损坏的文件,就要提示用户该文件已损坏。
写文件:
3、内容:读写中文或者其他语言内容时,会不会出现乱码字符格式问题。尤其在某个开发框架下,框架自身默认的字符编码格式,是否统一了系统调用的字符格式。如果用第三方库处理,这个库是否支持其它语言的读写。
4、内容长度:读写海量内容时,有些用户的计算机在性能方面上能不能得到保证?处理文件耗时多少?如果使用异步处理的话,修改的内容会不会错乱?
四、代码设计:
....
五、测试(预期结果):
····
六、开发总耗时:编码设计(1天) + 编码(1天) + 优化(2天) + 自测(1天) + 预留风险(1天)
临时加需求 |
你有没有遇到过这样的事情:当你好不容易把某个功能做好了,测试也快要完成了。结果产品经理急匆匆向你走过来,告诉你要加新需求。你心里原本刚落下沉重的石头,产品经理又给你提上了。
临时提出新需求,意味着你的开发周期受到影响,你的工作量加大,你原本设计好的代码结构可能也会被舍弃,然后重新设计。
《幕后产品:打造突破式产品思维》作者在她的书中也讲述过这个类似的经历。网易云音乐都快要上线了,结果为了某个新需求在竞争对手做好之前抢先一步,所以这个需求非加不可,她做出了让大家咬牙切齿的决定。于是召集大家说现在把这个新需求加上去,让大家赶紧开发,但还是按照原计划上线。
或许这种竞争激烈的情况,在互联网方向时常会发生,所以才会有 “产品经理与程序员是死对头”的传闻。
可是如何避免这个问题呢,我想不出一个彻底解决的方案。或许在开发的时候,尽量把代码结构设计好,才能尽可能的避免临时加需求所导致的问题。
二、 项目管理
把功能“最小”化,把产品做到“最小” |
产品经理设计新产品时,脑子里都会有个 MVP 的概念,即建立一个最小化可行产品。软件工程方法中也存在一个“快速原型模型”。两者都是为了用来快速验证产品是否满足用户需求,以及收集用户的反馈。
对于程序员,在编码中其实也要有类似这样的概念。我们开发产品一则要先实现最基本的功能模块,二则规避多写其他附属功能。这里的附属功能是我自己定义的词,是指围绕主要功能的附属功能,比如读写文件,只实现读写功能,但不要写如加载显示文件格式图标等等附属功能。在代码结构还不够成熟之前,千万不要多加附属功能,否则代码会变得复杂,会影响进一步优化代码结构。
从零到一的产品 |
从零到一的产品分为两种:一种是在已有产品的基础上,再开发一款具有相同性质的产品。另一种是完全是新业务新架构的项目。前者是可以使用软件复用的方式来开发新产品,而后者是完全没啥经验,是要大家准备踩坑的项目。
俗话说“鱼与熊掌不可兼得”。在项目管理中,生产率和生产质量不可兼得。但是在国内绝大多数公司的领导对产品开发的要求是两者兼具。结果是两者都要不得。
如果组成一个团队来开发一个公司从未有过的新产品,那么该团队必须要有一个对产品业务非常熟悉的人来主导整个软件过程。所以团队成员都是新手的化,千万不要把新项目都交给他们来处理。除非公司有耗得起时间成本的能力。
袋鼠和笼子的故事 |
领导曾经分享过一则寓言故事。每当遇到软件 bug 时,我就会想起了这个故事。
有一天,动物园管理员发现袋鼠从笼子里跑了出来,于是大家开会讨论,一致认为该问题是笼子的高度太低所致。所以他们决定把笼子的高度从10米提高到20米。但第二天,他们发现袋鼠还在外面,所以决定把高度提高到30米。
没想到隔天又看到了袋鼠跑到外面,于是他们大为紧张,决定一不做二不休,再把笼子的高度提高到100米。
一天长颈鹿和几只袋鼠在闲聊。长颈鹿问:“你们看。这些人会继续增加你的笼子吗?”
“很难说”,袋鼠,“如果他们继续忘记关门!”
这则故给了我们一个启发:找到问题所在,不要被表面的现象所迷惑,才能有效地解决问题
当然我们不能简单地悟出这样的道理。事实上,这个故事时常发生在我们的日常工作中。在开发过程中,我认为”遇到问题难以找到根源“是程序员排查问题的常事。即便是拥有丰富开发经验、再怎么聪明的老程序员也难逃这一劫。他们也会像动物园管理员那样,有时只会对出现可能性大的某处进行不断尝试的解决 。
那我们为什么处在这样的困境呢,又怎样才能摆脱这样的困境?
至于第一个问题,对此我分析了两点:
- 第一点:自身观念受限
我们对袋鼠的认识都局限在它本身的属性上。因为袋鼠是跳高能手,一般人会认为袋鼠跑出外面肯定是跳出来的。所以总是围绕这个所谓的关键问题上来解决。
比如程序员遇到网络通讯问题。用户A没法发送消息给用户B,一般按常识来说,问题一定出现在通讯连接上。于是乎,程序员一直都在这个问题的关键节点上排查,结果排查了几天都没有查出结果。有一天,他在调试代码的时候,发现调用某个第三方库的函数后,就直接跳过了通讯这一块。由此可知,这个地方才是造成通讯问题的最大凶手。
- 第二点:不熟悉业务
动物园管理员只会知道这个笼子是关袋鼠的,对笼子的构造一无所知。而设计笼子的人可能更清楚原因所在。当袋鼠跑出来之后,专业的人排查问题时,必然会考虑到笼门的设计问题。
而作为程序员,遇到难以解决问题时,要想想自己对该问题涉及到的技术和业务是否已经熟悉。
另外,管理员加到 100 米显然是不合理的,这是心存侥幸。例如在某个函数里要等待某个处理后的数据,才继续执行下一个指令时,程序员并不知道数据何时才能处理完,于是随便设置了一个延迟时间来解决。而这个程序放在低配置的计算机上,却出现了读取不到数据的问题。程序员又把延迟时间加长。这种方案显然是不合理的。
你平常写技术文档吗 |
其实做技术文档也要像编码那样,它也有自己的结构设计,具备可扩展和维护性。不能把大部分内容都写在文档里,也不要对项目结构过分解读。否则,随着项目迭代更新,亦或者对项目做了大变动重构之类的,那么原有的技术文档也就没有什么价值了。重新写一份?不可能的!
写技术文档主要侧重于软件架构、功能模块的说明以及一些重要的细节。
程序员应当做好这一点:少写文档(少写不该有的内容),少做注释(少写重复意思的注释),规范化代码结构。(把代码当作技术文档来写)
软件工程方法 |
注:原先这里是要写关于软件设计方法和测试的内容,涉及的点挺多,但是留给我的时间有限,就不写那么多了
三、编码设计
代码设计的抽象化 |
对于具有迭代性、需求变化多端的项目,我们对代码设计的要求复杂得多,因为更多地要考虑到如何应付产品将来变化的需求。
这是一道典型的简单算术题,用来考查答题者的逻辑能力。
题目:最近一家超市搞促销某宝矿泉水的活动:
顾客只需花1元可购买1瓶水。每2个空瓶能再换1瓶水。
如果一个顾客兜里有20元,最多能喝多少瓶水?
通常解题过程:
1)20元换来20瓶水,能喝20瓶。
2)20个空瓶,能再换成10瓶水,能喝10瓶。
3)10个空瓶,能换成5瓶水,能喝5瓶。
4)5个空瓶,能换2瓶水,还剩1个空瓶,能喝2瓶。
5)3个空瓶,能换1瓶水,还剩1个空瓶,能喝1瓶。
6)2个空瓶,能换1瓶水,能喝1瓶。
7)还剩1个空瓶。
所以最多能喝:20+10+5+2+1+1=39瓶水。
但抖机灵的人还想要向超市预支1瓶水,喝完了,把之前剩下的1个空瓶一起抵消给超市。又或者向别人借1个空瓶。所以最多能喝40瓶水。不过这个答案,我并不认可,因为超市和别人都不可能做亏本买卖。做亏本买卖,你觉得可能吗?要是说能变通,我岂不是最多可以喝无数瓶水。
这道题在软件开发中只能解出一个答案:39。
程序就应当有严谨的逻辑,最终一定要输出唯一的预期结果。借空瓶那是人情世故,所以在程序中是不允许出现借空瓶的 bug。
好了,既然程序只有一个答案,那我们该怎么设计代码呢?
如果按照刚从事编程这一饭碗的程序员的想法,他很有可能这样来设计的:
先列出一堆数据来分析
最初空瓶数 | 计算过程 | 最多能喝瓶水数 |
---|---|---|
0 | 0 | 0 |
1 | 1+1-1 | 1 |
2 | 2+2-1 | 2 |
3 | 3+3-1 | 5 |
4 | 4+4-1 | 7 |
… | … | … |
20 | 20+20-1 | 39 |
a | a+a-1 | 2a -1 |
从以上规律中,得出公式:
count =2*a-1;
他很高兴,认为自己的方法很高效,于是设计了以下的代码结构:
unsigned rmb = 20;
unsigned firstBottles = rmb * 1;//最初空瓶数
unsigned bottles = 2 * firstBottles - 1;
cout << "Bottles:" << bottles << endl;
这段代码的结构,如果运用在考试当中是合理的,但如果真正用在超市活动里,那是一场灾难。
这个代码逻辑虽然简洁,运算高效,但是潜在危险:
1)同事看不懂这段代码,如果不借助文档说明,都不知道这个公式是怎么得来的。
2)如果超市要调整活动方案,整个代码结构就没有用途了,就需要重新编写逻辑。
这道题如果站在专业程序员的角度来看,应当是这样子的:
【需求】
最近一家超市搞促销某宝矿泉水的活动:
顾客只需花 rmb_price 元可购买 bottles_price 瓶水。
每 empTyBottles 个空瓶能再换 newBottles 瓶水。
如果顾客兜里有 rmbs 元,最多能喝 canDrinkCount 瓶水?
【程序逻辑分析过程】
输入状态:现有 rmbs 元
条件:rmb_price 元 bottles_price 瓶水,empTyBottles 个空瓶换 newBottles 瓶水
输出状态:最多能喝 canDrinkCount 瓶水
【编码设计】
结构1(基本逻辑):“ rmb_price 元 bottles_price 瓶水”运算逻辑(输出状态:单价)
结构2(活动逻辑):“ empTyBottles 个空瓶换 newBottles 瓶水”运算逻辑(输出状态:能到顾客肚子的水瓶数量,以及无法兑换的空瓶数)
结构3(潜在逻辑):累计,用来分析活动的数据(输出状态:累计成本,空瓶数、利润,顾客之间存在交换的瓶数等)
说明:
1、结构1和结构2拆分出来单独处理。结构1在程序需求中是必然存在的,而结构2是可有可无,即如果商家不搞活动了或者活动过期,那么结构2就可以完全被移除掉,而不会影响到结构1.
2、结构三可以不用管,只是在结构一二的基础上,预留可扩展功能的空间就行。只是作为程序员也应当要考虑到的潜在需求,并不一定要编码设计结构三(若要设计,反而适得其反)。可以备注一下说明,提醒后来者有需求时可以在此处添加功能和设计。
3、当然针对结构二内部算法的具体实现,那是另外考验程序员的算法能力了
以上的代码设计中,我们只需要把那些如rmb_price变量作为用户接口,提供给用户来设置参数值。这样当老板说要调整活动方案后,只要设置这些参数就行了,根本就不需要我们动代码了。
何时重构 |
想想原本打算今天要完成这篇文章,同时后文会涉及到太多代码段了。(例如面向对象的设计、程序初始化处理等等)看来只能写到这里了。由于时间有限,估计往后有空了再补充吧。。。
转自: