前言
在上篇文章 要不要寫代碼注釋 中,我們闡述了為什么代碼需要注釋,但是在評論區(qū)中,有同學(xué)依然認(rèn)為簡介明了的代碼不需要注釋。本篇文章,我們通過舉兩個例子探討為什么我們認(rèn)為寫代碼需要寫注釋,歡迎大家給出建議。
例子一
同樣的命名,在不同場景下,返回字段含義不同
我司的業(yè)務(wù)系統(tǒng)中,會涉及到轉(zhuǎn)碼的業(yè)務(wù),將音視頻、PPT、Word、Excel等文件進行轉(zhuǎn)碼,然后使用播放器進行播放。在進行首抽象時,將轉(zhuǎn)碼后的資源統(tǒng)一抽象成了一個interface,代碼如下:
<?phpinterface Resource{ public function getName(); public function getPlayUrl(); public function getType(); public function getLength(); // 其他接口}
列出的三個接口,從命名角度上沒有問題,分別表達的是獲取資源的名稱、獲取資源的播放地址和獲取資源的長度。
但是在getLength這個接口的返回值上,是有歧義的,這個長度指的是什么?
如果是音視頻資源,我們很好理解,90%是指音視頻的時長,但是單位是秒還是分鐘,不確定。對于文檔類型的資源呢?首先,對于一個剛接觸這套系統(tǒng)的新人而言在不明白文檔資源到底被轉(zhuǎn)碼什么樣子的情況下,他是無法猜測其含義的,文檔有可能被轉(zhuǎn)成了視頻、圖片,再或者是其他格式。
我們的系統(tǒng)會將文檔資源轉(zhuǎn)成圖片或者html文件,一頁就是一張圖片或者一個html文件,這里的getLength,對于文檔資源來講,返回的是文檔資源的總頁數(shù)。
這里可能有同學(xué)會質(zhì)疑,為什么不將文檔和音視頻抽象成兩個不同的資源,針對文檔類型的,提供getTotalPage接口,獲取頁數(shù),針對音視頻類型的,提供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();}
原因很簡單特別簡單,抽象是為了把復(fù)雜的事情簡單化,而不是把簡單的事情復(fù)雜化!
這個場景中,除了getLength接口的返回值有歧義,其他接口針對不同類型資源是沒有歧義的。如果我們分別進行抽象,勢必會讓代碼更復(fù)雜,比如引入設(shè)計模式(工廠或策略,或者兩者皆有)。并且,調(diào)用資源的客戶端,也會引入條件分支,針對不同的資源類型,調(diào)用不同的方法。
‘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 ‘資源頁數(shù)是:’ . $res->getTotalPage();} else { echo ‘音視頻長度是:’ . $res->getLength() / 60 . ‘分鐘’;}
本來,我們增加一句注釋可以解決這個問題。
<?phpinterface Resource{ public function getName(); public function getPlayUrl(); public function getType(); /** * 對于流媒體資源,入視頻、音頻等,返回時長,單位為秒; * 對于文檔類型資源,返回文檔的頁數(shù). * * @return int */ public function getLength(); // 其他接口}
也有同學(xué)會說,我不搞復(fù)雜化,所有資源都是getLength接口,具體的返回值,可以看代碼實現(xiàn)。如果這樣的話,這就會有以下問題,一是違背了好命名的初衷,為什么我們要用好的方法名,就是為了一眼能看出函數(shù)的意圖,二是增加了他人的負(fù)擔(dān),閱讀實現(xiàn)代碼既費時又痛苦,沒有隱藏細(xì)節(jié)(關(guān)于隱藏細(xì)節(jié)這個話題,我們會單獨寫一篇文章進行介紹)。
代碼無法表達一些隱藏的邏輯
在接觸過的業(yè)務(wù)中,有一個學(xué)習(xí)平臺。對于課程的設(shè)計大概是這樣的:
有一張課程表 course,用于記錄課程的基本信息
有一張course_member表,用于記錄課程中的用戶,主要字段為課程id course_id和用戶id user_id。
還有一張 course_learn_record表,用戶記錄用戶學(xué)習(xí)時長,包含課程id course_id字段、用戶id user_id字段 和學(xué)習(xí)時長字段。
在課程的業(yè)務(wù)中實現(xiàn)中,有一個方法,代碼如下
getCourseMemberDao() ->findCourseMemberIds($courseId); return $this->getCourseLearnRecordDao() ->countLearnTimeByCourseIdAndMemberIds($courseId, $memberIds); } }
該方法先從 course_member 表中查了一次課程下所有的用戶id,再根據(jù)課程id和用戶id去course_learn_record 表中統(tǒng)計學(xué)習(xí)時長。
為什么要先查一遍課程的用戶id,再把課程id和用戶id組成與關(guān)系,去course_learn_record表中統(tǒng)計數(shù)據(jù)呢?單通過課程id,也可以統(tǒng)計出課程下所有用戶的學(xué)習(xí)時長,為何多此一舉?
原來,業(yè)務(wù)要求,課程的成員可以被移除、也允許被再次添加到課程;當(dāng)成員再次添加到課程的時候,需要保留之前的學(xué)習(xí)記錄。
因此,這個方法統(tǒng)計的是 課程當(dāng)前成員的學(xué)習(xí)時長。
有同學(xué)會說,這明顯是方法命名不對,那什么樣的命名可以體現(xiàn)出這個邏輯呢?
我們內(nèi)部有同學(xué)提出了兩個命名:
一是 countCourseCurrentMembersLearnTime, 直譯就是統(tǒng)計課程當(dāng)前成員的學(xué)習(xí)時長;
另外一個是 countCourseValidLearnTime, 直譯是統(tǒng)計課程有效的學(xué)習(xí)時長;
兩個命名都不好。首先,統(tǒng)計課程當(dāng)前成員的學(xué)習(xí)時長,這個“當(dāng)前”是怎么來的,為什么會有當(dāng)前成員這種說法,是否還有“非當(dāng)前”的成員存在,對于不熟悉業(yè)務(wù)的開發(fā)同學(xué)來講,是比較蒙的,可能要問一圈才知道為什么。同理,統(tǒng)計課程有效的學(xué)習(xí)時長,為什么要區(qū)分有效和無效,什么情況下有效,什么情況下無效?
(PS: 如果你有更好的命名方式,可以貼在評論區(qū))
如果我們給個注釋,情況就很明了:
getCourseMemberDao() ->findCourseMemberIds($courseId); return $this->getCourseLearnRecordDao() ->countLearnTimeByCourseIdAndMemberIds($courseId, $memberIds); } }
總結(jié)
良好的命名能減少代碼對注釋的依賴,但仍有大量設(shè)計信息無法用代碼表示。