前言
在上篇文章 要不要写代码注释 中,我们阐述了为什么代码需要注释,但是在评论区中,有同学依然认为简介明了的代码不需要注释。本篇文章,我们通过举两个例子探讨为什么我们认为写代码需要写注释,欢迎大家给出建议。
例子一
同样的命名,在不同场景下,返回字段含义不同
我司的业务系统中,会涉及到转码的业务,将音视频、PPT、Word、Excel等文件进行转码,然后使用播放器进行播放。在进行首抽象时,将转码后的资源统一抽象成了一个interface,代码如下:
<?phpinterface Resource{ public function getName(); public function getPlayUrl(); public function getType(); public function getLength(); // 其他接口}
列出的三个接口,从命名角度上没有问题,分别表达的是获取资源的名称、获取资源的播放地址和获取资源的长度。
但是在getLength这个接口的返回值上,是有歧义的,这个长度指的是什么?
如果是音视频资源,我们很好理解,90%是指音视频的时长,但是单位是秒还是分钟,不确定。对于文档类型的资源呢?首先,对于一个刚接触这套系统的新人而言在不明白文档资源到底被转码什么样子的情况下,他是无法猜测其含义的,文档有可能被转成了视频、图片,再或者是其他格式。
我们的系统会将文档资源转成图片或者html文件,一页就是一张图片或者一个html文件,这里的getLength,对于文档资源来讲,返回的是文档资源的总页数。
这里可能有同学会质疑,为什么不将文档和音视频抽象成两个不同的资源,针对文档类型的,提供getTotalPage接口,获取页数,针对音视频类型的,提供getLenght接口,并提取出公共方法抽象成基类?
<?php// 针对不同资源进行抽象接口示例interface BaseResource { public function getName(); public function getPlayUrl(); public function getType();}interface MediaResource extends BaseResource{ public function getLenght();}interface DocResource extends BaseResource{ public function getTotalPage();}
原因很简单特别简单,抽象是为了把复杂的事情简单化,而不是把简单的事情复杂化!
这个场景中,除了getLength接口的返回值有歧义,其他接口针对不同类型资源是没有歧义的。如果我们分别进行抽象,势必会让代码更复杂,比如引入设计模式(工厂或策略,或者两者皆有)。并且,调用资源的客户端,也会引入条件分支,针对不同的资源类型,调用不同的方法。
‘MediaResource’, ‘doc’ => ‘DocResource’, ];public static function getResource($type, $resouceId) { if (!isset(self::$map[$type]) { throw new Exception(sprintf(‘Not support type %s’, $type)); } $resClass = self::$map($type); return new $resClass($resourceId); }}// Client端$res = ResourceFactory::getResource($resouceId);if (‘doc’ == $res->getType) {echo ‘资源页数是:’ . $res->getTotalPage();} else { echo ‘音视频长度是:’ . $res->getLength() / 60 . ‘分钟’;}
本来,我们增加一句注释可以解决这个问题。
<?phpinterface Resource{ public function getName(); public function getPlayUrl(); public function getType(); /** * 对于流媒体资源,入视频、音频等,返回时长,单位为秒; * 对于文档类型资源,返回文档的页数. * * @return int */ public function getLength(); // 其他接口}
也有同学会说,我不搞复杂化,所有资源都是getLength接口,具体的返回值,可以看代码实现。如果这样的话,这就会有以下问题,一是违背了好命名的初衷,为什么我们要用好的方法名,就是为了一眼能看出函数的意图,二是增加了他人的负担,阅读实现代码既费时又痛苦,没有隐藏细节(关于隐藏细节这个话题,我们会单独写一篇文章进行介绍)。
代码无法表达一些隐藏的逻辑
在接触过的业务中,有一个学习平台。对于课程的设计大概是这样的:
有一张课程表 course,用于记录课程的基本信息
有一张course_member表,用于记录课程中的用户,主要字段为课程id course_id和用户id user_id。
还有一张 course_learn_record表,用户记录用户学习时长,包含课程id course_id字段、用户id user_id字段 和学习时长字段。
在课程的业务中实现中,有一个方法,代码如下
getCourseMemberDao() ->findCourseMemberIds($courseId); return $this->getCourseLearnRecordDao() ->countLearnTimeByCourseIdAndMemberIds($courseId, $memberIds); } }
该方法先从 course_member 表中查了一次课程下所有的用户id,再根据课程id和用户id去course_learn_record 表中统计学习时长。
为什么要先查一遍课程的用户id,再把课程id和用户id组成与关系,去course_learn_record表中统计数据呢?单通过课程id,也可以统计出课程下所有用户的学习时长,为何多此一举?
原来,业务要求,课程的成员可以被移除、也允许被再次添加到课程;当成员再次添加到课程的时候,需要保留之前的学习记录。
因此,这个方法统计的是 课程当前成员的学习时长。
有同学会说,这明显是方法命名不对,那什么样的命名可以体现出这个逻辑呢?
我们内部有同学提出了两个命名:
一是 countCourseCurrentMembersLearnTime, 直译就是统计课程当前成员的学习时长;
另外一个是 countCourseValidLearnTime, 直译是统计课程有效的学习时长;
两个命名都不好。首先,统计课程当前成员的学习时长,这个“当前”是怎么来的,为什么会有当前成员这种说法,是否还有“非当前”的成员存在,对于不熟悉业务的开发同学来讲,是比较蒙的,可能要问一圈才知道为什么。同理,统计课程有效的学习时长,为什么要区分有效和无效,什么情况下有效,什么情况下无效?
(PS: 如果你有更好的命名方式,可以贴在评论区)
如果我们给个注释,情况就很明了:
getCourseMemberDao() ->findCourseMemberIds($courseId); return $this->getCourseLearnRecordDao() ->countLearnTimeByCourseIdAndMemberIds($courseId, $memberIds); } }
总结
良好的命名能减少代码对注释的依赖,但仍有大量设计信息无法用代码表示。