篇首语:工作了这么多年,袁帅心存的美好一直没有消亡,他始终相信没有人愿意去恶意撒谎,即便是不那么善良的人,撒谎也不一定有多好受。他觉得,凡是需要撒谎的地方,很有可能存在苦衷,再说,撒一个谎言,还要用另一个谎言去圆。累,本来就这么卷了,何必再让自己内心这么累。

幸运的是,袁帅在Thoughtworks这个环境里,他的初心得到了最大程度的呵护,在跟人相处上,他整体上对自己和同事都比较满意。可是有时候,看到一些代码在撒谎,他很想教育一把代码。这不,年会上跟清扬的过招让他意犹未尽,反正他也有义务带清扬,正好最近在跟不同业务部门合作过程中,他发现了一些问题,借此机会,他想拉着清扬一起来教育一下代码。

代码不讲真话的直接后果是所有人被误导了,然后做了一件错误的事情,不自知地将错就错,让错误越陷越深,最后浪费宝贵的时间。可不讲真话,编写代码的人又不是故意的,也万万不可上纲上线,他秉着内训师作为知识沉淀者和文化传播者角色的原则,借助教育代码的机会组织了一次部门内部的闲聊会,并美其名曰:Clean Code交流会。


讲真话了吗?

周五,是一个心情放松的日子,距离年会过去也快整整一周了。袁帅也趁此机会召集了团队的几名还在开发在线学习系统的小伙伴来闲聊侃大山,看起来是一次不经意的安排,其实袁帅是早有准备的。为了让氛围更好,他安排了清扬作为托,并嘱咐:该出手时就出手,不该出手就创造出手的时机。

伪修饰

下午2:45的样子,5位小伙伴聚齐在一个不大不小的会议室,桌子上提前布置了一些坚果零食和水果饮料。有心的人都能看出来袁帅自掏腰包了,把自己平日健身的坚果补给贡献出来。大家格外积极主动起来,颇让袁帅感到欣慰。

袁帅开门见山:“最近跟北美一个BP在协商给他们大客户团队的骨干成员强化敏捷工程实践,现在很多部门知道咱们内训有一套敏捷工程实践精品工作坊课程。但是很有意思的是,这个BP跟我线上会议沟通时,从来不一步到位,总会在微信上再跟我发文字再解释一遍,也不知道是我看着傻,还是她认为我痴呆。”

大家被袁帅这开场给逗乐了,抛给他好奇的目光。

袁帅故作一脸无辜,刚要继续说,就被清扬打断了:“这有啥,没讲清楚呗,再给你讲一遍。”

“说得好,她觉得没讲清楚,想再跟我解释一遍,而且每次微信上的留言比她会议上讲的要更明确更具体。所以,后来慢慢地我习惯在会议上忽视她提供的信息,直接看她的微信留言。”

“那你们还干嘛开会呀,不是白费时间吗!” 万正义童鞋直率地补充道。“可是,有时候她在后续的会议上又把之前的内容给更新了却忘了给我微信留言。” 袁帅继续无辜。

“所以,你仍然按照微信留言的信息,结果误解了真实的意思!” 清扬又抢答成功。

“嗐,你还别提,我最近都没跟哪位真人神仙打过交道,也总是遇到这种烦恼。” 刘欢欢童鞋一脸满腹牢骚的表情。

“是嘛,说来听听?”袁帅内心窃喜欢欢中了自己的圈套,故作惊讶般试探道。

“注释啊,最近看到一些代码的注释,本来不看注释花点时间琢磨琢磨代码,还能搞清楚代码在做什么,可是有时想走捷径,瞅了眼注释,NND,最后发现注释是错的,骗的我团团转,被谎言带节奏真不爽!” 欢欢打趣地补充。

“对的,这就跟我提到的那个BP类似,总习惯加一些修饰,本来初心是好的,是想解释得更清楚,可是修饰只是一种补充信息,有时候原始信息变更了,修饰却没有跟着更新,不但没有起到修饰作用,反而会误导别人,很耽误事儿!” 袁帅同情地回复了正义。

说白了,还是没有想清楚,代码要做什么事情,然后觉得需要通过注释去解释一下,后面代码改了,大概率是不会修改注释的。” 正义总是话里充满着正义。

“我很久前上培训班的时候,有个老师告诉我们要为代码写注释,我后面就记住了,自己也经常写注释,而且我上一份工作,客户还以代码行数来衡量我们的产出,多一行注释,就多一点产出呢。” 王天乐也参与到讨论中来,不过他现在深知这个认知是存在缺陷的。

“可能是使用中文注释读起来更顺畅吧,因为代码不太好用中文命名”。这时候程晓娜以理解的口吻弱弱地补充了一句。袁帅突然陷入了沉思,脑海里浮现出两个画面:

  1. 他曾经辅导过的某些程序员,因为英语功底不好,很难将中文翻译成恰当的英文,这在一定程度限制了他们,即便有翻译工具的辅助。
  2. 有一次为了给一个方法起名字,他跟三个10多年的工作经验的技术Leader一起讨论了10来分钟,最后才搞定,但大家很开心。

“很有可能哦,好在现在有很多翻译工具,比如google翻译、有道翻译能够帮很大的忙,当然程序员加强英语基础也是很有必要的。”袁帅自己都觉得有点莫名其妙地插了一句。

为什么要写注释啊?代码自解释不香吗?只有代码没法自解释的时候才用一下注释还能接受。把注释当做代码产出,别闹,生产毒瘤!” 正义越发正义起来,直接打断了袁帅的讲话,气氛突然有点紧张起来。

“可有时方法太长了,做的事情比较多,每一段相关的代码用注释说明一下在做什么也不是不可取的吧?”清扬想来救个场,抛了个问题。

“来来来,咱们来看几段代码,Talk is cheap,show me the code!”袁帅故意模仿魔兽世界里面的Boss声音,夸张的表情一出来,大家很配合地一本正经地回应道:“Give me your code”,说完大家东倒西歪的笑起来。随即,袁帅娴熟地把准备好的代码投到屏幕上:

  1. //---- 示例1 ----//
  2. /*
  3. Author:张三
  4. Date:2015-03-15
  5. */
  6. public class Order {
  7. // 构造器,构建Order对象时要传入订单项
  8. public Order(List<OrderItem> orderItems){}
  9. }
  10. //---- 示例2 ----//
  11. public interface OrderRepository {
  12. // 按订单状态查找订单
  13. public List<Order> find(Status status);
  14. }
  15. //---- 示例3 ----//
  16. public class OrderReceipt {
  17. private Order order;
  18. public OrderReceipt(Order order) {
  19. this.order = order;
  20. }
  21. // 打印订单发票
  22. public String printReceipt() {
  23. StringBuilder output = new StringBuilder();
  24. // 打印头
  25. output.append("======Printing Orders======\n");
  26. // 打印日期,订单号和客户忠诚度
  27. print date, bill no, customer name
  28. // output.append("Date - " + order.getDate();
  29. output.append(o.getCustomerName());
  30. output.append(o.getCustomerAddress());
  31. // output.append(order.getCustomerLoyaltyNumber());
  32. // 打印订单条目
  33. double totSalesTx = 0d;
  34. double tot = 0d;
  35. for (LineItem lineItem : o.getLineItems()) {
  36. output.append(lineItem.getDescription());
  37. output.append('\t');
  38. output.append(lineItem.getPrice());
  39. output.append('\t');
  40. output.append(lineItem.getQuantity());
  41. output.append('\t');
  42. output.append(lineItem.totalAmount());
  43. output.append('\n');
  44. // 计算交易税额,税率按照15%
  45. double salesTax = lineItem.totalAmount() * .10;
  46. totSalesTx += salesTax;
  47. // 计算订单总金额 = 价格 * 数量 + 15%的交易税额
  48. tot += lineItem.totalAmount() + salesTax;
  49. }
  50. // 打印总税额
  51. output.append("Sales Tax").append('\t').append(totSalesTx);
  52. // 打印总价
  53. output.append("Total Amount").append('\t').append(tot);
  54. return output.toString();
  55. }
  56. }

“5分钟时间,大家先仔细阅读这段代码,把你看到的问题和建议改进措施发到咱们的讨论群里哈。”袁帅说完喝了口水润嗓子。

5分钟后,袁帅把所有人的答案汇总起来:

  • 示例1:类上的注释完全没必要,因为VCS工具能够很好地做记录。
  • 示例1:构造方法方法上的注释是冗余的,构造器本身就能表达构造对象,参数也能表达传入的东西。
  • 示例2:方法的注释的确起到了解释作用,但是让方法自解释更香:更名为listByStatus后干掉注释。
  • 示例3:方法太长,中间分段注释解释做什么的可以抽取方法,然后将注释转换成方法名来表达意图。
  • 示例3:printReceipt方法内打印日期和客户忠诚度注释过期了,实际上代码被注释掉了。
  • 示例3:printReceipt方法内那个税率写的是15%,而实际用的10%,误导读者。

“当然咱们也不能走极端,注释并不是一无是处,毕竟那谁讲过 — 存在即合理。注释存在这么多年,而且很多地方咱们也看到过是吧。” 袁帅又恢复了主持人的状态。

“有啊,开放API文档到处都是API注释,而且还要写好看了。”

“最近有几段代码实在有些实现的难言之隐,我必须通过注释来解释一下。”

“嗯,我刚碰到过法律版本信息的一些注释,白纸黑字的注释声明不能少。”

“魔术代码可以注释一下啊,比如复杂的邮件正则表达式:/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;

袁帅对最后这一个补充有点感兴趣,趁机提了个问题:“魔术代码除了注释,还有更好的方式吗?”

引入解释性变量啊,比如:emailMatcher。”清扬这次临场发挥反应很快。

“大家怎么看待TODO | FIXME这种注释呢?”清扬紧接着有抛了个问题。

“不提倡,虽然它能帮助我们在本地记录一些代办列表,但尽可能及时处理完这些任务,将注释清理掉,最好别提交到生产上。”正义的声音覆盖了全场。

袁帅朝清扬和正义点了个大拇指赞,清了清嗓子,开启最后的总结:“还是有一些场景需要注释的,今天我拐弯抹角说这么多,如果要浓缩成一句话,我觉得是:当我们想要添加注释的时候,我们首先应该想想代码能否做到自解释。

在收拾电脑之际,他脑海里浮现出Uncle Bob关于注释的一句话:

注释不同于《辛德勒的名单》。它们不是“纯善的”。事实上,注释充其量是一种必要的恶。 — Robert C.Martin

行外话

名字没有表达真实业务含义,懒得多花时间思考,懒得动用google翻译,懒得跟业务人员沟通,先起了个名字把,后期再改也行,反正代码是改出来的…

很久前,袁帅在老马(Martin Fowler)的博客上看到一句话:「There are only two hard things in Computer Science: cache invalidation and naming things.」。这句话不是老马本人讲的,老马表示很认同,当然袁帅也表示认同,而且伴随编码经验越多,越坚信。

之前在交付项目上,袁帅对Clean Code执念很深,却乐在其中。受《Java编码规范》、《整洁代码之道》、《编写可读代码的艺术》、《修改代码的艺术》这几本启蒙读物的“毒害”,他跟命名结下了不解之缘,不论大小,不分场合。


距离上一次的Clean Code交流会过去刚好一周,今天天气格外晴朗,清扬饶有兴致喊上袁帅去楼底下新开的小餐馆炒俩菜。两人来到餐厅刚坐定,袁帅手机微信语音响起来,是兄弟团队TL石彪,想找他聊聊上次TDD工作坊后大家的落地情况和疑惑。

清扬伸手递过菜单示意袁帅点个菜,只见袁帅没看菜单就说了个“青菜”就继续跟石彪聊了起来。而这边清扬在菜单上找了好一会儿也没找到名叫“青菜”的菜,见袁帅聊得投入,她就没继续追问,自己做主给点了个绿色的白灼生菜,紧接着点了个清蒸桂花鱼,外加两份单人份的土鸡汤和米饭。

10分钟后,青菜上来了,两个人开始吃起来,吃凉两口,袁帅觉着不对劲:“咦,我点的是青菜啊,怎么上了个生菜呢?”

“大哥,我尽力了,菜单没有找到“青菜”,就点了个这咯。” 清扬装作无辜道。

“哦,我们那有一种叫“上海青”的蔬菜,平时我们都叫青菜。”袁帅说完立即意识到自己现在身在北方,这边的叫法跟老家可能不一样。

“在西安,我们把青菜理解成绿色的蔬菜哦。”还没等袁帅回过神,清扬凑到他耳旁小声地说,随即就拿起菜单递给袁帅让他自己瞅瞅。袁帅快速扫了一眼,从图片中找到一个叫“清炒油菜”的图片就是他想要的青菜。

盯着“清炒油菜”,袁帅发呆了一小会,心里琢磨着:“不同地区(上下文),对同一个东西的叫法是可能不一样的,如果切换了地区,自己还沿用原来地区的叫法,很可能造成困惑和误解。

不同行业,不同领域,不同上下文,同物可不同名。你常常给我讲写代码的时候要特别注意这一点,开发哪个领域的系统,就应该使用该领域特定的业务语言,这样有利于统一语言,交流起来效率会高很多,而且代码跟业务更匹配,更容易理解。” 清扬猜到了袁帅在琢磨什么,替袁帅做了一个总结。袁帅两眼瞪得大大地惊讶地盯着清扬,放下筷子,对她竖起了两只手的大拇指。

“你们的清蒸桂花鱼,请慢用!” 服务员把香气四溢的鱼端到桌上,这是袁帅小时候特别喜欢的一道菜,他开心地拿起筷子正要去夹鱼尾:“咦,刚才服务员叫它清蒸桂花鱼?” “对啊,桂花鱼,清蒸的,营养健康,色香味俱全。”清扬麻利地回应着,袁帅拿起菜单看了一眼,嘴里边嘟囔边若有所悟地点头着:“哦,在我老家这个菜叫清蒸鳜鱼。”

清扬瞥了他一眼,作摇头叹息状伸手去夹鱼,开心地吃了起来。“小鬼厉害啊,竟然点了我最喜欢的鱼,这顿饭我请了哈~” 袁帅这次快速从他的菜品命名的思绪中跳脱出来。


周五下午是团队的学习分享时间,袁帅环视了一周,看到5个小伙伴兴致勃勃的在围着一台显示器学习练习Bob大叔的TDD,而清扬在那里望着窗外发呆。他眼珠子咕噜一转,走过去拍了清扬的肩膀:“喂,小鬼又做白日美梦啦,走,咱们去兄弟团队转转?” 清扬略显尴尬但随即起身跑在了前头,正好赶上兄弟团队5个人围着大电视屏幕在做Code Review。

袁帅之前辅导过这个团队做TDD、重构、持续集成,跟TL石彪很熟,中午吃饭前还跟石彪聊了一会。见袁帅和清扬过来,石彪微笑示意以表欢迎,他俩也回以微笑,悄悄地靠近了电视屏幕,尽量不打扰到其他人。

屏幕上的代码是团队中看起来比较青涩的小伙伴在分享,他应该是这周刚来的新人,袁帅和清扬还不认识他,他俩目光迅速切换到屏幕上:

@Entity
public class Flight {
    private String id;
    private String flightNumber;
    private FlyLine flyLine;
    // ...
}


public class FlyLine {
    private Location from;
    private Location to;
    private double distance;
    // ...
}

“小豹,这个FlyLine是指飞行路线吗?” 石彪小心翼翼地问。

“嗯,差不多,我写的FlyLine代表航线,”

“这样子啊,你在IDE搜一下【Route】” 只见小豹比较娴熟地用快捷键定位到一个Route类:

@Entity
public class Route {
    @Id
    private Location from;
    private Location to;
    private double distanceKM;

    // ...
}

看到这个类,小豹快速在正开着的Google翻译框里查到【Route:航线】。

“你打开Trello卡上的【航空术语】那张卡片上的有个航空术语链接。你打开页面敲关键字【航线】进行搜索。”石彪见小豹下意识挠着头不知所措,就进一步给了提示。

石彪10秒钟的指尖操作后,大屏幕上将页面展示出来:

"航线" 搜索到1个领域术语,4个偏差术语

航线:
 - 领域术语:Route
 - 偏差术语:[FlyRoute, FlyLine, FlightLine, AirLine]

袁帅注意到小豹脸有点泛红,估计他此时有些尴尬和小紧张。

“没关系,小豹刚来,对这些航空行业术语不熟悉可以理解,我提前给大家分享一条原则:入行说行话,既然咱们在开发航空系统,我们就要讲航空行话。” 石彪刻意扫视了所有在场的小伙伴,继续说道:“那怎么学会这些行话呢,翻译工具是一个好助手,但有时它也会不灵,那怎么办呢?咱们团队一直有在维护了一套专门的术语,上周我刚把它开发成Web版部署在内网服务器上了,咱们随时可以在上面检索术语哈。

石彪说完,小豹立刻叮嘱结对的小伙伴在卡片上记下了一个Action:【更换Flight类中引用Flyline —> Route,】


袁帅心里头很是为石彪的开发的【系统业务术语在线检索】赞许,此时他想起了之前在一个保险客户上看到一个代码片段,正要掏出手机去找,只见欢欢急匆匆走过来凑到袁帅耳旁:“袁帅老师,我们在练习TDD时遇到了一个命名的问题需要你支持一下。”

随即袁帅悄悄地离开了Code Review会场,清扬见状带着一副若有所思的表情跟了过来。

“袁帅老师,我们刚才看了TDD视频,手痒就想在学习系统实战一把,但遇到几个类和方法不知道怎么命名好?” 见元帅过来,天乐就边指着屏幕边说着。袁帅凑到屏幕看了一小会儿,略微一笑,故作思考状:“这个确实有点难啊,一般遇到这种命名问题,怎么解决呢?”

“我们用了好几种翻译工具,翻译的结果不一样,不知道取哪个好。”欢欢积极回应。

“咱们有没有术语表啊?”清扬好奇问道。“我记得之前正义就牵头搞了一个”,她立马回答了自己的问题,并看向正义。

“查了,术语表还没有这个词。”正义两手一摊。

袁帅紧接着抛出了第二个问题:“既然没查到,这时候是不是找人确认一下比较好,既然是…”

对啊,咱们可以去找学习系统的领域专家聊聊啊,喏,不就在那里吗!” 天乐等不及袁帅讲完,像捡了个宝一样开心地看向坐在桌子一端的BA小姐姐佟婕。

袁帅见他们几个一窝蜂围了过去,知道他们将会获得一个理想的答案,便自个回到座位上,刚坐定,清扬凑了过来:“诶,刚才兄弟团队那个场面感觉搞得新人有点尴尬,不就是一个小小的命名吗,干嘛要那么较真?”

“写代码最重要的点是什么?” 袁帅认真地问起来。

“揭示意图啊!”清扬脱口而出,脑海里想起来之前跟袁帅讨论过kent Beck简单设计的四原则。

在正确满足业务功能的大前提下,尽可能消除重复代码,尽可能让代码揭示意图,然后尽可能减少不必要的代码元素” 清扬兴奋地跟上这一句。袁帅意会到她的意思了,立马回以大加赞赏的表情,紧接着问道:“揭示意图核心点是什么?”

“准确的表达业务概念呗!”清扬撇撇嘴。

“没错,怎么就算准确表达了业务概念呢?一个核心点是准确使用行业的专有业务术语,这样大家都用同一套业务语言,沟通协作也自然顺畅很多。” 袁帅喝了口水,加重了声调。“千万不要小看命名哈,在一个业务稍微复杂的系统,如果出现很多术语表达同一个概念,就乱套了。如果把架构没比作人体骨架和肌肉韧带,命名就像体内的血液,两者对人的健康都至关重要。我之前辅导一个保险行业客户进行遗留系统改造,那里面的命名混乱简直是要命!”

作为TL,石彪刚才抓住了这种团队社交机会来拉齐团队认知,并且他还将知识部署到内网服务器上,让大家消费起来很便捷,这样就能很好地促进知识转化成团队的认知。”袁帅一鼓作气,用赞赏的目光望了一眼正专注投入的石彪。

“至于你说的新人小豹有点尴尬,我也感觉到了,不过石彪做的还比较柔和,这种好时机可不能错过,有时候宁可牺牲短暂的和谐,只要团队信任度在,问题不大。在敏捷开发中,衡量很多社交活动有没有效的一个点是会不会发生积极的变化,这也是咱以后跟团队开会要去关注的,冲突引发改变 over 和谐保持原样。”袁帅表示了对清扬的理解。

清扬对小豹的同情得到了袁帅的理解,对袁帅的话也深表了赞同:“确实,BA(业务分析师)和PO(产品负责人)是这方面的领域专家,开发人员在遇到业务语言的问题应该及时找他们沟通对齐,然后将这些知识积累维护起来,让血液始终保持健康很重要。

“小鬼不错啊,这些有用的知识日积月累会大大提升团队的研发效能。我得出发了,今晚7:00有篮球活动。”说完袁帅背上双肩包,朝门外一路小跑。

“团队研发效能?这是啥?”清扬好学的嗓音直追袁帅而去,被挡在了电梯外。