《Clean Code》 代碼簡潔之道

做者介紹

原文做者: Robert C. Martin, Object Mentor公司總裁,面向對象設計、模式、UML、敏捷方法學和極限編程領域的資深顧問,是《敏捷軟件開發:原則、模式、與實踐》的做者。
翻譯做者:韓磊,互聯網產品與運營專家,技術書籍著譯者。譯著有《夢斷代碼》和《C#編程風格》等。(居然不是程序員~~~)javascript

內容概要

本書後幾章主要講了java相關的類、系統、和併發的設計介紹,較粗略,與簡潔之道不是特別融合,故而省略,想要詳細瞭解的建議去看更優質的詳細講解。
本書主要站在代碼的可讀性上討論。可讀性? 顧名思義,代碼讀起來簡潔易懂, 讓人心情愉悅,大加讚揚。
在N年之後,本身或者他人仍然可以稍加閱讀就能明白其中的意思。什麼是整潔代碼?看看程序員鼻祖們怎麼說的,php

  1. 整潔代碼只作好一件事。—— Bjarne Stroustrup, C++語言發明者css

  2. 整潔代碼從不隱藏設計者的意圖。—— Grady Booch, 《面向對象分析與設計》做者html

不能想着,這代碼我能看懂就行了, 即便當下能看懂,那幾個月甚至一年之後呢。更不能說爲了體現本身編程「高大上」,故意用一些不爲人知的語法,如java

const LIMIT = 10
const LIMIT = Number(++[[]][+[]]+[+[]])
  1. 儘量少的依賴關係,模塊提供儘可能少的API。—— Dave Thoms, OTI創始人, Eclipse戰略教父
  2. 代碼應該經過其字面表達含義,命名和內容保持一致。 —— Michael Feathers, 《修改代碼的藝術》做者
  3. 減小重複代碼,甚至沒有重複代碼。—— Ron Jeffries, 《C#極限編程探險》做者
  4. 讓編程語言像是專門爲解決那個問題而存在。 —— Ward Counningham, Wiki發明者

​​

有意義的命名

  • 名副其實

好的變量、函數或類的名稱應該已經答覆了全部的大問題,若是須要註釋補充,就不算名副其實。
工具函數內部的臨時變量能夠稍微能接收。node

// what's the meaning of the 'd'?
int d;
// what's list ?
List<int []> list;

int daysSinceCreation
int daysSinceModification

  

此處的重定向,起名爲「redirection」會不會更好一點,laravel

/**
 * 重定向
 */
public function forward(Request $request, $controller, $action) {}
/**
 * 重定向
 */
public function default(Request $request,  $controller, $action) {}

  

既是註冊賬號,爲什麼不直接命名爲 register 呢?也許會說,註冊就是新增賬號,create也是新增賬號,天然,create能夠表明註冊。可新增賬號多是本身註冊,也多是系統分配,還多是管理員新增賬號,業務場景不同,實現也極可能不同。因此,建議取一個明確的,有意義的,一語道破函數幹了啥的名稱。git

//註冊帳號
public function create($data) {}

  

  • 避免誤導

程序員必須避免留下掩藏代碼本意的錯誤線索。變量命名包含數據類型單詞(array/list/num/number/str/string/obj/Object)時,需保證該變量必定是該類型,包括變量函數中可能的改變。更致命的誤導是命名和內容意義不一樣,甚至徹底相反。程序員

// 肯定是 List?
accountList = 0
// 肯定是 Number?
nodeNum = '1'

//肯定全部狀況返回值都是list嗎?
function getUserList (status) {
    if (!status) return false
    let userList = []
    ...
    return userList
}
.align-left {
  text-align: "right";
}

  

  • 作有意義的區分

product/productIno/productData 如何區分?哪一個表明哪一個意思? Info 和 Data就像 a / an / the 同樣,是意義含糊的廢話。以下函數命名也是沒有意義的區分,github

getActiveAccount()
getActiveAccounts()
getActiveAccountInfo()

  

  • 使用讀的出來的名稱

讀不出來就不方便記憶,不方便交流。大腦中有很大一塊地方用來處理語言,不利用起來有點浪費了。

  • 使用可搜索的名稱

讓IDE幫助本身更便捷的開發。假如在公共方法裏面起個變量名叫value,全局搜索,而後一臉懵逼地盯着這上百條搜索結果。 (value vs districts)

  • 每一個概念對應一個詞

媒體資源叫media resources 仍是 publisher?

  • 添加有意義的語境

firstName/lastName/street/city/state/hourseNumber
=>
addrFirstName/addrLastName/addrStreet/addrCity/addrState/hourseNumber

註釋

什麼也比不上放置良好的註釋來的有用。
什麼也不會比亂七八糟的註釋更有本事搞亂一個模塊。
什麼也不會比陳舊、提供錯誤信息的註釋更有破壞性。
若編程語言足夠有表達力,或者咱們長於用這些語言來表達意圖,就不那麼須要註釋——也根本不須要。

  • 做者爲何極力貶低註釋?

註釋會撒謊。因爲程序員不能堅持維護註釋,其存在的時間越久,離其所描述的代碼越遠,甚至最後可能全然錯誤。不許確的註釋比沒有註釋壞的多,淨瞎說,真實只在一處地方存在:代碼。

  • 註釋的恰當用法是彌補咱們在用代碼表達意圖時遭遇的失敗。
// 禁用、解凍
public function option(Request $request) {}
// 記錄操做日誌
protected function writeLog($optType,$optObjectName, $optId, $optAction) {}

  

=>

protected function recordOperationLog($optType,$optObjectName, $optId, $optAction) {}

  

將上面的 註釋 + 代碼 合成下方純代碼,看着更簡潔,且不會讀不懂。
再者,能夠在函數定義的地方添加說明性註釋,可不能在每一個用到這個函數的地方也添加註釋,這樣,在閱讀函數調用的環境時,還得翻到定義的地方瞅瞅是什麼意思。但若是函數自己的名稱就能描述其意義,就不存在這個問題了。
別擔憂名字太長,能準確描述函數自己的意義纔是更重要的。

  • 註釋不能美化糟糕的代碼。
    對於爛透的代碼,最好的方法不是寫點兒註釋,而是把它弄乾淨。與其花時間整一大堆註釋,不如花時間整好代碼,用代碼來闡述。
// check the obj can be modified
if (obj.flag || obj.status === 'EFFECTIVE' && user.info.menu === 1) {
    // todo
}
if (theObjCanBeModified()) {}
function theObjCanBeModified () {}

  

好註釋

  1. 少量公司代碼規範要求寫的法律相關注釋。

/**
 * Laravel IDE Helper Generator
 *
 * @author    Barry vd. Heuvel <barryvdh@gmail.com>
 * @copyright 2014 Barry vd. Heuvel / Fruitcake Studio (http://www.fruitcakestudio.nl)
 * @license   http://www.opensource.org/licenses/mit-license.php MIT
 * @link      https://github.com/barryvdh/laravel-ide-helper
 */

namespace Barryvdh\LaravelIdeHelper;

  

  2. 對意圖的解釋,如,

function testConcurrentAddWidgets() {
...
// this is our best attempt to get a race condition
// by creating large number of threads.
for (int i = 0; i < 25000; i++) {
 // to handle thread
}
}

  3. 闡釋
  有時,對於某些不能更改的標準庫,使用註釋把某些晦澀難懂的參數或返回值的意義翻譯爲某種可讀的形式,也會是有用的。

function compareTest () {
  // bb > ba
  assertTrue(bb.compareTo(ba) === 1) 
  // bb = ba
  assertTrue(bb.compareTo(ba) === 0) 
  // bb < ba
  assertTrue(bb.compareTo(ba) === -1) 
}
// could not find susan in students.
students.indexOf('susan') === -1

  

  4. 警示
  註釋用於警告其餘程序員某種後果,也是被支持的。

  函數,

// Don't run unless you have some time to kill
function _testWithReallyBigFile () {}

  文件頂部註釋,

/**
 * 文件來內容源於E:\Git_Workplace\tui\examples\views\components\color\tinyColor.js,須要新增/編輯/刪除內容請更改源文件。
 */

  5. TODO

  來不及作的,使用TODO進行註釋。雖然這個被容許存在,但不是無限書寫TODO的理由,須要按期清理。

  6. 放大

  註釋能夠用來放大某些看着不合理代碼的重要性。

  不就是個trim()麼?

// the trim is real importan. It removes the starting
// spaces that could casuse the item to be recoginized
// as another list
String listItemContent = match.group(3).trim()

  

  沒引入任何編譯後的js和css,代碼如何正常工做的呢?請看註釋。

<body>
  <div id="app"></div>
  <!-- built files will be auto injected -->
</body>

  

  7. 公共API中的DOC
  公共文檔的doc通常會用於自動生成API幫助文檔,試想若是一個公共庫沒有API說明文檔,得是一件多麼痛苦的事兒,啃源碼花費時間實在太長。

 

壞註釋

  1. 喃喃自語
    寫了一些除了本身別人都看不懂的文字。

  2. 多餘的註釋
    簡單的函數,頭部位置的註釋全屬多餘,讀註釋的時間比讀代碼的時間還長,徹底沒有任何實質性的做用。

    // Utility method that returns when this.closed is true.
    // Throws an exception if the timeout is reached.
    public synchronized void waitForClose(final long timeoutMillis)
    throw Exception {
    if (!closed) 
    {
     wait(timeoutMillis);
     if (!closed)
       throw new Exception("MockResponseSender could not be closed");
    }
    }
    

      

  3. 誤導性註釋
    代碼爲東,註釋爲西。

  4. 多餘的註釋

  
// 建立
public function create(Request $request) {}
// 更新
public function update(Request $request) {}
// 查詢
public function read(Request $request) {}
// 刪除
public function delete(Request $request) {}

  

  $table已經初始化過了,@var string 這一行註釋看上去彷佛就沒那麼必要了。

/**
 * The table name for the model.
 * @var string
 */
protected $table = 'order_t_creative';

  

  5. 括號後面的註釋

  只要遵循函數只作一件事,儘量地短小,就不須要以下代碼所示的尾括號標記註釋。

try {
  ...
  while () {
   ...
  } // while
  ...
} // try
catch () {
  ...
} // catch

  

  通常不在括號後方添加註釋,代碼和註釋不混在一行。

function handleKeydown (e) {
  if (keyCode === 13) { // Enter
    e.preventDefault()
    if (this.focusIndex !== -1) {
      this.inputValue = this.options[this.focusIndex].value
    }
    this.hideMenu()
  }
  if (keyCode === 27) { // Esc
    e.preventDefault()
    this.hideMenu()
  }
  if (keyCode === 40) { // Down
    e.preventDefault()
    this.navigateOptions('next')
  }
  if (keyCode === 38) { // Up
    e.preventDefault()
    this.navigateOptions('prev')
  }
}

  

現做出以下調整,

function handleKeydown (e) {
  const Enter = 13
  const Esc = 27
  const Down = 40
  const Up = 38
  e.preventDefault()
  switch (keycode) {
    case Enter:
      if (this.focusIndex !== -1) {
        this.inputValue = this.options[this.focusIndex].value
      }
      this.hideMenu()
      break
    case Esc:
      this.hideMenu()
      break
    case Down:
      this.navigateOptions('next')
      break
    case Up:
      this.navigateOptions('prev')
      break
  }
}

  

  經過定義數字變量,不只去掉了註釋,各個數字也有了本身的意義,再也不是魔法數字,根據代碼環境,幾乎不會有人問,「27是什麼意思?」 諸如此類的問題。再者,if狀況過多,用switch代替,看着稍顯簡潔。最後,每個都有執行了e.preventDefault(),能夠放在switch外層,進行一次書寫。

  6. 歸屬和署名
  源碼控制系統很是善於記住誰在什麼時候幹了什麼,沒有必要添加簽名。新項目能夠清除地知道該和誰討論,但隨着時間的推移,簽名將愈來愈不許確。
固然,這個也見仁見智,支付寶小程序抄襲微信小程序事件的觸發即是由於代碼裏面出現開發小哥的名字。若是爲了版權須要,法律聲明,我想寫上做者也是沒有什麼大問題的。

/**
 * Created by PhpStorm.
 * User: XXX
 * Date: 2017/9/29
 * Time: 14:14
 */

namespace App\Services;
use Cache;
class CacheService implements CacheServiceInterface
{
}
/**
 * 功能: 廣告位管理
 * User: xxx@tencent.com
 * Date: 17-8-2
 * Time: 下午4:47
 */
class PlacementController extends BaseController
{
}

  

  7. 註釋掉的代碼
  直接把代碼註釋掉是討厭的作法。Don’t do that! 其餘人不敢刪除註釋掉的代碼,可能會這麼想,代碼依然在那兒,必定有其緣由,或者這段代碼很重要,不能刪除。
其餘人由於某些緣由不敢刪能夠理解,但若是是本身寫的註釋代碼,有啥不敢刪呢?再重要的註釋代碼,刪掉後,還有代碼控制系統啊,這個系統會記住人爲的每一次改動,還擔憂啥呢?放心地刪吧!管它誰寫的。

// $app->middleware([
//    App\Http\Middleware\DemoMiddleware::class
// ]);

// $app->routeMiddleware([
//     'auth' => App\Http\Middleware\Authenticate::class,
// ]);

if (APP_MODE == 'dev') {
    $app->register(Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class);
}
$app->register(\App\Providers\UserServiceProvider::class);
$app->register(\App\Providers\UserRoleServiceProvider::class);

  8. 信息過多

  9. 別在註釋中添加有趣的歷史性話題或無關的細節描述。

  10. 註釋和代碼沒有明顯的聯繫

  11. 註釋和代碼之間的聯繫應該顯而易見,若是註釋自己還須要解釋,就太糟糕了。

/**
* start with an array that is big enough to hold all the pixels
* (plus filter biytes), and extra 200 bytes for header info
*/
this.pngBytes = new byte[((this.width + 1) + this.height * 3) + 200];

  

  12. 非公共代碼的doc類註釋

  有些doc類的註釋對公共API頗有用,但若是代碼不打算做公共用途,就沒有必要了。

下面的四行註釋,除了第一行,其它的都顯得不少餘,無疑在重複函數參數已經描述過的內容。假若閱讀代碼的人花了時間看註釋,結果啥也沒有,沮喪;知道沒用自動掠過,沒有花時間看註釋,那這注釋還留着幹啥。

/**
 * 根據媒體ID獲取廣告位ID
 * @param PlacementService $service
 * @param Request $request
 * @return Msg
 */
public function getPublisherPlacementIds(PlacementService $service, Request $request) {}

  

函數

  • 短小

函數第一規則是要短小,第二規則是還要更短小。if語句,else語句,while語句等,其中的代碼塊應該只有一行。函數代碼行建議不要超過20行,每行代碼長度建議150個字符左右。以下代碼片斷,建議換行。

export default function checkPriv (store, path) {
  return store.state.user.privileges && (store.state.user.privileges.includes(path) || store.state.user.privileges.includes(`/${path.split('/')[1]}/*`) || isAll(store))
}

  

  • 函數應該只作一件事,作好這件事。

  以下函數,executeSqlContent() 很明顯不止作一件事, 前半部分實現了鏈接配置的獲取,後半部分根據config執行sql。

/**
 * 根據文件名和文件路徑執行具體的sql
 * @param $file
 * @param $dbConfigPath
 * @param $date
 */
protected function executeSqlContent($file, $dbConfigPath, $date)
{
    $config = [];
    // 獲取數據庫名稱
    if ($file == 'nn_global.sql' || $file == 'nn_pub_template.sql') {
        // DB配置
        $config = config("database.connections.global");
        $userId = 'global';

    } elseif (strpos($file, 'nn_pub') !== false) {
        $fileName = explode('.', $file);

        $dbName = explode('_', $fileName[0]);
        if (count($dbName) == 3) {
            $dbInfo = UserDbTConfig::select(['onsn_name'])->where('dbn_name', $fileName[0])->first();
            if ($dbInfo) {
                $dbInfo = $dbInfo->toArray();
                $onsInfo = zkname($dbInfo['onsn_name']);
                $config = config("database.connections.individual");
                // 覆蓋HOST
                $config['host'] = $onsInfo->ip;
                $config['port'] = $onsInfo->port;
                $userId = $dbName[2];
            }
        }
    }

    if ($config) {
        // sql語句
        $dbSqlConfig = file_get_contents($dbConfigPath . $file);
        if ($dbSqlConfig) {
            $this->info($file . '文件內容爲:' . $dbSqlConfig);

            // 添加新的鏈接
            config(["database.connections.pp_pub_{$userId}" => $config]);
            $db = DB::connection("nn_pub_{$userId}");
            $db->statement($dbSqlConfig);

            // 執行成功,文件備份移動
            $dirName = 'static/bak/' . $date;
            if (!is_dir($dirName)) {
                mkdir($dirName, 0777, true);
            }
            $cmd = "mv " . $dbConfigPath . $file . "  " . $dirName;
            shell_exec($cmd);

            // 斷開DB鏈接
            DB::disconnect("nn_pub_{$userId}");

            $this->info($file . '文件內容爲執行完成');
        }
    }
}

  

  • 每一個函數一個抽象層級,函數中混着不一樣抽象層級每每容易讓人迷惑。

  以下代碼即是抽象層級不同, getConnectionConfig() ,屬於已經抽象過的一層函數調用,下方的文件處理倒是具體的實現。
舉這個例子只是爲了說明不一樣的抽象層級是這個意思,因爲函數自己不復雜,不存在讓人迷惑的問題。
只是函數實現一旦混雜多了,不容易搞得清楚某一行表達式是基礎概念仍是細節,更多的細節就會在函數中糾結起來。

protected function executeSqlContent($file, $dbConfigPath, $date) { $config = $this->getConnectionConfig($file) if ($config) { // sql語句 $dbSqlConfig = file_get_contents($dbConfigPath . $file); if ($dbSqlConfig) { $this->info($file . '文件內容爲:' . $dbSqlConfig); // 添加新的鏈接 config(["database.connections.pp_pub_{$userId}" => $config]); $db = DB::connection("nn_pub_{$userId}"); $db->statement($dbSqlConfig); // 執行成功,文件備份移動 $dirName = 'static/bak/' . $date; if (!is_dir($dirName)) { mkdir($dirName, 0777, true); } $cmd = "mv " . $dbConfigPath . $file . " " . $dirName; shell_exec($cmd); // 斷開DB鏈接 DB::disconnect("nn_pub_{$userId}"); $this->info($file . '文件內容爲執行完成'); } } } private function getConnectionConfig ($file) { $config = [] // 獲取數據庫名稱 if ($file == 'nn_global.sql' || $file == 'nn_pub_template.sql') { // DB配置 $config = config("database.connections.global"); $userId = 'global'; } elseif (strpos($file, 'nn_pub') !== false) { $fileName = explode('.', $file); $dbName = explode('_', $fileName[0]); if (count($dbName) == 3) { $dbInfo = UserDbTConfig::select(['onsn_name'])->where('dbn_name', $fileName[0])->first(); if ($dbInfo) { $dbInfo = $dbInfo->toArray(); $onsInfo = zkname($dbInfo['onsn_name']); $config = config("database.connections.individual"); // 覆蓋HOST $config['host'] = $onsInfo->ip; $config['port'] = $onsInfo->port; $userId = $dbName[2]; } } } return $config } 

  稍好一點的抽象層級以下,固然excuteSql()還能夠繼續拆分,當書寫函數的時候須要打空行來區別內容的大部分時候 能夠考慮拆分函數了。

protected function executeSqlByFile($file, $dbConfigPath, $date) { if ($this->getConnectionConfig($file)) { $this->excuteSql($file, $dbConfigPath, $date) } } private function getConnectionConfig($file) { $config = [] // 獲取數據庫名稱 if ($file == 'nn_global.sql' || $file == 'nn_pub_template.sql') { // DB配置 $config = config("database.connections.global"); $userId = 'global'; } elseif (strpos($file, 'nn_pub') !== false) { $fileName = explode('.', $file); $dbName = explode('_', $fileName[0]); if (count($dbName) == 3) { $dbInfo = UserDbTConfig::select(['onsn_name'])->where('dbn_name', $fileName[0])->first(); if ($dbInfo) { $dbInfo = $dbInfo->toArray(); $onsInfo = zkname($dbInfo['onsn_name']); $config = config("database.connections.individual"); // 覆蓋HOST $config['host'] = $onsInfo->ip; $config['port'] = $onsInfo->port; $userId = $dbName[2]; } } } return $config } private function excuteSql($file, $dbConfigPath, $date) { $dbSqlConfig = file_get_contents($dbConfigPath . $file); if ($dbSqlConfig) { $this->info($file . '文件內容爲:' . $dbSqlConfig); config(["database.connections.nn_pub_{$userId}" => $config]); $db = DB::connection("nn_pub_{$userId}"); $db->statement($dbSqlConfig); $dirName = 'static/bak/' . $date; if (!is_dir($dirName)) { mkdir($dirName, 0777, true); } $cmd = "mv " . $dbConfigPath . $file . " " . $dirName; shell_exec($cmd); DB::disconnect("nn_pub_{$userId}"); $this->info($file . '文件內容爲執行完成'); } } 
  • 使用描述性的函數名

  長而具備描述性的名稱,比短而使人費解的名稱好。(若是短也能,固然更好)
  長而具備描述性的名稱,比描述性長的註釋好。代碼維護時,大多數程序員都會自動忽略掉註釋,不能保證每次更改都實時更新,越日後越不想看註釋,由於極可能造成誤導,程序纔是真事實。
  因此,別怕長,更重要的是描述性,看到這個函數名稱就知道是幹啥的。讀代碼就像是讀英文文章同樣,先幹了啥,後幹了啥,細節怎麼幹的?

  小竅門:可使用IDE搜索幫助完善命名。

  即便結合文件名,publisherController,打死我也沒法將 all 和 移動媒體分類 聯繫起來。建議函數名:getMobileMediaClassification()

/**
 * 移動媒體分類
 */
public function all(PublisherServices $service, Request $request) {}

  

  完美命名示範,代碼上方的註釋或許已經不須要了,不過對於母語是中文的咱們來講,就當是英文翻譯了。

/**
 * 根據媒體ID獲取廣告位ID
 */
public function getPublisherPlacementIds(PlacementService $service, Request $request)

  

  • 函數參數

最理想的參數數量是0,其次是一,再次是二,應儘可能避免三。除非有足夠的理由,不然不要用三個以上的參數了。
參數多於兩個,測試用例覆蓋全部的可能值組合是使人生畏的。
避免出現輸出參數。

  • 標識參數。

向函數傳入布爾參數簡直就是駭人聽聞的作法,這樣作,就是大聲宣佈函數不止作一件事,爲true會這樣,爲false會那樣。非Boolean類型「標識」參數同理。

以下代碼明確地指出initOrder進行了兩種徹底不一樣的初始化方式。

// 訂單數據初始化分兩種,一種爲普通建立訂單,一種爲經過庫存轉下單
function initOrder(flag) {
  if (flag === true) {
    // normalInit
    // ...
  } else {
    // init order by inventory
    // ..
  }
}

  

改進以下,也許你會說,initOrder不仍是幹了兩件事兒嗎?不,它不是本身幹了這兩件事兒,它只是負責叫別人幹這兩件事。
若是能夠的話,initOrder裏面的判斷甚至能夠放在能直接拿到flag的地方。

function initOrder(flag) {
  flag === true ? this.normalInit() : this.initOrderByInvenroty()
}

function normalInit () {
  // todo
}

function initOrderByInvenroty () {
  // todo
}

  

excuteSql($file, $dbConfigPath, $date) 中的參數 $dbConfigPath 和 $filefile_get_contents()的做用下變成了標識參數
$dbSqlConfig爲真就執行主體函數,爲假就不執行。

private function excuteSql($file, $dbConfigPath, $date)
{
    $dbSqlConfig = file_get_contents($dbConfigPath . $file);
    if ($dbSqlConfig) {
        $this->info($file . '文件內容爲:' . $dbSqlConfig);

        config(["database.connections.pp_pub_{$userId}" => $config]);
        $db = DB::connection("nn_pub_{$userId}");
        $db->statement($dbSqlConfig);

        $dirName = 'static/bak/' . $date;
        if (!is_dir($dirName)) {
            mkdir($dirName, 0777, true);
        }
        $cmd = "mv " . $dbConfigPath . $file . "  " . $dirName;
        shell_exec($cmd);

        DB::disconnect("nn_pub_{$userId}");
        $this->info($file . '文件內容爲執行完成');
    }
}

  

  改進以下,將標識參數拎出函數具體實現,

protected function executeSqlByFile($file, $dbConfigPath, $date)
{
    if ($this->getConnectionConfig($file) && $this->file_get_contents($dbConfigPath . $file)) {
        $this->excuteSql($file, $dbConfigPath, $date)
    }
}

  

  • 分隔指令與詢問

函數要麼作什麼,要麼回答什麼,但兩者不可兼得。函數應該修改某對象的狀態或是返回該對象的相關信息,兩樣都幹就容易致使混亂。

從讀者的角度思考,set是指是否已經設置過呢?仍是設置成功呢?

if (set("username", "unclebob")) ...

  

也許上述代碼名稱能夠更爲 setAndCheckExists , 但依舊沒有解決實質性地問題,最好是將指令和詢問分隔開來,代碼以下,

if (attributeExists("username")) {
  setAttribute("username", "unclebob")
}

  

  • 使用異常替代返回錯誤碼

  錯誤處理代碼能從主路徑中分離出來,閱讀的時候,能夠直面主路徑內容。

Promise.all([
  InventoryService.read({job_id: this.jobId}),
  this.getPlacementType()
]).then((result) => {
  let [inventoryInfo] = result
  if (res.$code !== 0) {
    this.$alert.error(res.$msg)
    this.$loading(false)
  } else {
    let ret = this.getReserveInfo(data)
    if (ret.reservable) {
      this.orderInitFromInventory(inventoryInfo.$data, this.defaultOrderInfo)
    } else {
      this.$alert.error('該庫存不能下單,可能緣由:庫存未計算完成!')
      this.$loading(false)
    }
  }
})
Promise.all([
  InventoryService.read({job_id: this.jobId}),
  this.getPlacementType()
]).then((result) => {
  try {
    let [inventoryInfo] = result
    this.checkResponseCode(inventoryInfo)
    this.isInventoryCanBeOrdered(inventoryInfo.$data)
    this.orderInitFromInventory(inventoryInfo.$data, this.orderInfo)
  } catch (err) {
    this.$alert.error(err.message)
    this.$loading(false)
  }
})

isInventoryCanBeOrdered (data) {
  let ret = this.getReserveInfo(data)
  if (!ret.reservable) {
    throw Error('該庫存不能下單,可能緣由:庫存未計算完成!')
  }
}

checkResponseCode (res) {
  if (res.$code !== 0) {
    throw Error(res.$msg)
  }
},

  

  • 別重複本身。

  重複多是軟件中一切邪惡的根源。許多原則與實踐都是爲控制與消除重複而建立。

created () {
  this.$setServiceLoading('數據初始化中...')
  let tplPromise = this.getCreativeTplList({})
  let p1 = new Promise((resolve, reject) => {
    publisherService.getAll({
      op_status: 'ENABLE'
    }).then(res => {
      if (res.$code !== 0) {
        reject()
      } else {
        this.publisherOptions = res.$data
        resolve()
      }
    })
  })
  let p2 = new Promise((resolve, reject) => {
    publisherService.selectAllRules().then(res => {
      if (res.$code !== 0) {
        reject()
      } else {
        this.protectionOptions = res.$data
        resolve()
      }
    })
  })
  let p3 = new Promise((resolve, reject) => {
    realizeService.selectAllRules().then(res => {
      if (res.$code !== 0) {
        reject()
      } else {
        this.realizeOptions = res.$data
        resolve()
      }
    })
  })
  Promise.all([p1, p2, p3, tplPromise]).then(() => {
    if (this.$route.query.id) {
      this.isEditMode = true
      placementService.read({placement_id: this.$route.query.id}).then((res) => {
        if (res.$code !== 0) {
          this.$alert.error(res.$msg, 3000)
        } else {
          Object.assign(this.formData, res.$data)
          Object.keys(this.formData).forEach(key => {
            if (typeof this.formData[key] === 'number') {
              this.formData[key] += ''
            }
          })
          this.$nextTick(() => {
            res.$data.creative_tpl_info.forEach((tpl) => {
              this.formData.tpls[this.formData.placement_type][tpl.creative_tpl_type].checked.push(tpl.creative_tpl_id)
            })
            this.updateCreativeIds()
          })
        }
      }, () => {
        this.$router.replace({path: '/placement/list'})
      })
    }
  }, () => {
    this.$alert.error('初始化媒體信息失敗...')
    this.$router.replace({path: '/placement/list'})
  })
}

  

  消除重複代碼,

created () {
  if (!this.$route.query || !this.$route.query.id) return
  this.$setServiceLoading('數據初始化中...')
  Promise.all([
    publisherService.getAll({ op_status: 'ENABLE' }),
    publisherService.selectAllRules({}),
    realizeService.selectAllRules({}),
    this.getCreativeTplList({})
  ]).then((resData) => {
    if (!this.checkResCode(resData)) return
    let [publisherOptions, protectionOptions, realizeOptions] = resData
    this.publisherOptions = publisherOptions
    this.protectionOptions = protectionOptions
    this.realizeOptions = realizeOptions
    this.isEditMode = true
    placementService.read({placement_id: this.$route.query.id}).then((res) => {
      if (!this.checkResCode([res])) return
      Object.assign(this.formData, res.$data)
      Object.keys(this.formData).forEach(key => {
        if (typeof this.formData[key] === 'number') {
          this.formData[key] += ''
        }
      })
      this.$nextTick(() => {
        res.$data.creative_tpl_info.forEach((tpl) => {
          this.formData.tpls[this.formData.placement_type][tpl.creative_tpl_type].checked.push(tpl.creative_tpl_id)
        })
        this.updateCreativeIds()
      })
    })
  })
}

function checkResCode (resData) {
  for (let i = 0, len = resData.length; i < len; i++) {
    let res = resData[i]
    if (res.$code !== 0) {
      this.$alert.error(`初始化媒體信息失敗,${res.$msg}`, 3000)
      this.$router.replace({path: '/placement/list'})
      return false
    }
  }
  return true
}

  

  • 別返回null,也別傳遞null

  javascript中,須要返回值的,別返回null/undefined,也別傳遞null/undefined,除非特殊須要。
一旦返回值存在null,就意味着每個調用的地方都要判斷、處理null,不然就容易出現不可預料的狀況。 以下方代碼所示,

public void registerItem(Item item) {
  if (item !== null) {
    ItemRegistry registry = peristentStore.getItemRegistry();
    if (registry != null) {
      Item existing = registry.getItem(item.getID());
      if (existing.getBillingPeriod().hasRetailOwner()) {
        existing.register(item);
      }
    }
  }
}

  

因此,在本身能夠控制的函數中(不可控因素如:用戶輸入),別返回null,也別傳遞null,別讓空判斷搞亂了代碼邏輯。


  • 綜合案例

根據《clean code》來看,下面這個函數有如下幾個方面須要改進,

  1. 函數太大
  2. 代碼重複
  3. 函數命名不具備描述性
  4. 部分註釋位置放置不合適
  5. 某些行字符數量太多

 

//註冊帳號
public function create($data)
{
    //檢查是否能夠註冊
    $check = [
        'tdd'        => $data['tdd'],
    ];
    foreach ($check as $field => $value) {
        $exist = $this->userService->check($field, $value);
        if($exist) {
            throw new RichException(Error::INFO_UNIQUE, [[Error::INFO_UNIQUE,['QQ']]]);
        }
    }
    $userId = $data['user_id'];
    //檢查主帳號是否存在
    $exist = $this->userService->check('user_id', $userId);
    if(!$exist) {
        throw new RichException(Error::INFO_NOT_FIND_ERROR);
    }
    //姓名帳號內惟一
    $exist = (new UserModel($userId))->where('operate_name', '=', $data['operate_name'])->where('user_id', '=', $userId)->where('deleted', '=', 0)->first();
    if($exist) {
        throw new RichException(Error::INFO_UNIQUE, [[Error::INFO_UNIQUE,['姓名']]]);
    }

    $time = date('Y-m-d H:i:s');
    //基本信息
    $exist = (new UserModel($userId))->where('tdd', '=', $data['tdd'])->where('user_id', '=', $userId)->where('deleted', '=', 1)->first();
    if($exist) {
        (new UserModel($userId))->where('tdd', '=', $data['tdd'])->update([
            'operate_name'  => $data['operate_name'],
            'remarks'   => isset($data['remarks']) ? $data['remarks'] : '',
            'tdd'        => $data['tdd'],
            'time'  => $time,
            'operate_status' => UserModel::DEFAULT_STATUS,
            'user_id'   => $userId,
            'deleted'   => 0,
        ]);
    } else {
        (new UserModel($userId))->insert([
            'operate_name'  => $data['operate_name'],
            'remarks'   => isset($data['remarks']) ? $data['remarks'] : '',
            'tdd'        => $data['tdd'],
            'time'  => $time,
            'operate_status' => UserModel::DEFAULT_STATUS,
            'user_id'   => $userId,
            'deleted'   => 0,
        ]);
    }
    //刪除帳號一樣能夠建立
    $exist = (new UserQQModel())->where('tdd','=', $data['tdd'])->where('deleted', '=', 1)->first();
    if($exist) {
        (new UserQQModel())->where('tdd', '=', $data['tdd'])->update([
            'tdd' => $data['tdd'],
            'user_id' => $userId,
            'user_type' => UserInfoModel::USER_TYPE_OPT,
            'time' => $time,
            'deleted'   => 0,
        ]);
        //刪除原角色信息
        (new OptUserRoleModel($userId))->where('tdd','=', $data['tdd'])->delete();
    } else {
        (new UserQQModel())->insert([
            'tdd' => $data['tdd'],
            'user_id' => $userId,
            'user_type' => UserInfoModel::USER_TYPE_OPT,
            'time' => $time,
            'deleted'   => 0,
        ]);
    }
    //角色信息
    if(isset($data['role_ids']) && is_array($data['role_ids'])) {
        $OptRole = array();
        foreach ($data['role_ids'] as $item) {
            if($item) {
                $opt = [
                    'user_id'   => $userId,
                    'tdd'    => $data['tdd'],
                    'role_id'   => $item,
                    'time'  => $time,
                ];
                $OptRole[] = $opt;
            }
        }
        //更新角色數量信息---暫時不作維護
        if($OptRole) {
            (new OptUserRoleModel($userId))->insert($OptRole);
        }
    }
    //記錄日誌
    $operateType = BusinessLogConst::CREATE;
    $operateObjectName = $data['operate_name'];
    $operateId = $data['tdd'];
    $operateAction = ['operate_name' => $data['operate_name'], 'remarks'   => isset($data['remarks']) ? $data['remarks'] : '', 'user_id'   => $userId, 'role_ids' => isset($data['role_ids']) ? json_encode($data['role_ids']) : ''];
    $res = $this->writeLog($operateType, $operateObjectName, $operateId, $operateAction);

    return ['user_id' => $userId, 'tdd' => $data['tdd']];
}

  

調整後,晃一眼 registerAccount 就能知道函數幹了啥,1. 可否註冊判斷;2. 建立賬號; 3.記錄註冊日誌。多麼完美的閱讀感覺。

public function registerAccount($data)
{
    $this->canBeRegister($data);
    $this->createAccount($data);
    $this->recordRegisterLog($data);
    return ['user_id' => $data['user_id'], 'tdd' => $data['tdd']];
}

private function canBeRegister($data)
{
    $this->isQqExist ($data);
    $this->isPrimaryAccountExist($data);
    $this->isUsernameUnique($data);
}

private function isQqExist($data)
{
    $check = [
        'tdd' => $data['tdd'],
    ];
    $exist = false
    foreach ($check as $field => $value) {
        $exist = $this->userService->check($field, $value);
        if($exist) {
            throw new RichException(Error::INFO_UNIQUE, [[Error::INFO_UNIQUE,['QQ']]]);
        }
    }
    return $exist
}

private function isPrimaryAccountExist($data)
{
    $userId = $data['user_id'];
    $exist = $this->userService->check('user_id', $userId);
    if(!$exist) {
        throw new RichException(Error::INFO_NOT_FIND_ERROR);
    }
    return $exist
}

private function isUsernameUnique($data)
{
    $userId = $data['user_id'];
    $exist = (new UserModel($userId))->where('operate_name', '=', $data['operate_name'])->where('user_id', '=', $userId)->where('deleted', '=', 0)->first();
    if($exist) {
        throw new RichException(Error::INFO_UNIQUE, [[Error::INFO_UNIQUE,['姓名']]]);
    }
    return $exist
}

private function createAccount($data)
{
    $this->createAccounInUserModel($data)
    $this->createAccountInUserQqModel($data)
    $this->updateRoleInfo($data)
}

private function createAccounInUserModel($data)
{
    $userInfo = [
        'operate_name'  => $data['operate_name'],
        'remarks'   => isset($data['remarks']) ? $data['remarks'] : '',
        'tdd'        => $data['tdd'],
        'time'  => $time,
        'operate_status' => UserModel::DEFAULT_STATUS,
        'user_id'   => $userId,
        'deleted'   => 0,
    ]
    $exist = (new UserModel($userId))->where('tdd', '=', $data['tdd'])->where('user_id', '=', $userId)->where('deleted', '=', 1)->first();
    if($exist) {
        (new UserModel($userId))->where('tdd', '=', $data['tdd'])->update($userInfo);
    } else {
        (new UserModel($userId))->insert($userInfo);
    }
}

private function createAccountInUserQqModel($data)
{
    $userInfo = [
        'tdd' => $data['tdd'],
        'user_id' => $userId,
        'user_type' => UserInfoModel::USER_TYPE_OPT,
        'time' => $time,
        'deleted'   => 0,
    ]
    $exist = (new UserQQModel())->where('tdd','=', $data['tdd'])->where('deleted', '=', 1)->first();
    if($exist) {
        (new UserQQModel())->where('tdd', '=', $data['tdd'])->update($userInfo);
        (new OptUserRoleModel($userId))->where('tdd','=', $data['tdd'])->delete();
    } else {
        (new UserQQModel())->insert($userInfo);
    }
}

private function updateRoleInfo($data)
{
    if(isset($data['role_ids']) && is_array($data['role_ids'])) {
        $OptRole = array();
        foreach ($data['role_ids'] as $item) {
            if($item) {
                $opt = [
                    'user_id'   => $userId,
                    'tdd'    => $data['tdd'],
                    'role_id'   => $item,
                    'time'  => $time,
                ];
                $OptRole[] = $opt;
            }
        }
        //更新角色數量信息---暫時不作維護
        if($OptRole) {
            (new OptUserRoleModel($userId))->insert($OptRole);
        }
    }
}

private function recordRegisterLog($data)
{
    $operateType = BusinessLogConst::CREATE;
    $operateObjectName = $data['operate_name'];
    $operateId = $data['tdd'];
    $operateAction = ['operate_name' => $data['operate_name'], 'remarks'   => isset($data['remarks']) ? $data['remarks'] : '', 'user_id'   => $userId, 'role_ids' => isset($data['role_ids']) ? json_encode($data['role_ids']) : ''];
    $res = $this->writeLog($operateType, $operateObjectName, $operateId, $operateAction);
}

  

 

沒有人能一開始就寫出完美的程序,完美的系統,都是經過一點點改進達到一個更好的狀態。

格式

今天編寫的功能,極有可能在下一版本中被修改,但代碼的可讀性卻會之後可能發生的修改行爲產生深遠影響。即便代碼可能已經不存在,但歷史風格及其律條扔能存活下來。

垂直格式

  • 向報紙學習

寫的好的報紙文章,從上往下閱讀,在頂部,指望有個條目,告訴咱們故事主題,以便讓咱們決定是否要繼續讀下去。第一段是整個故事的大綱,給出粗線條概述,但隱藏了故事細節,接着讀,細節漸次增長,知道你瞭解全部的日期、名字、引語、說法及其餘細節。
源文件也要像報紙文章那樣,名稱應當簡單且一目瞭然。源文件頂部給出高層次概念和算法,細節往下漸次展開,直到找到源文件中最底層的函數和細節。

  • 垂直方向上的區隔

幾乎全部的代碼都是從上往下讀,從左往右讀。每行展示一個表達式或一個句子,每組代碼航展現一條完整的思路。這些思路用空白行區隔開來。

根據以下兩段,能夠看看添加空白行和不添加空白行的閱讀體驗,

package fitnesse.wikitext.widgets;
import java.util.regex.*;
public class BoldWidget extends ParentWidget {
  public static final String REGEXP = "'''.+?'''";
  private static final Pattern pattern = Pattern.compile("'''.+?'''",
    Patter.MULTILINE + Pattern.DOTALL
  );
  public BoldWidget(ParentWidget parent, string text) throw Exception {
    super(parent);
    Matcher match = pattern.matcher(text);
    match.find();
    addChildWidgets(match.group(1));
  }
  public String render() throws Exception {
    StringBuffer html = new StringBuffer("<b>");
    html.append(childHtml).append("</b>");
    return html.toString()
  }
}

  

閱讀代碼通常是一部分一部分地理解,然,看見上面那麼大一部分,第一感覺,就不是很友好,第一眼便有種預感要花不少時間理解,放棄的情緒很容易就此而生。且再看看下面這段代碼,

package fitnesse.wikitext.widgets;

import java.util.regex.*;

public class BoldWidget extends ParentWidget {
  public static final String REGEXP = "'''.+?'''";
  private static final Pattern pattern = Pattern.compile("'''.+?'''",
    Patter.MULTILINE + Pattern.DOTALL
  );

  public BoldWidget(ParentWidget parent, string text) throw Exception {
    super(parent);
    Matcher match = pattern.matcher(text);
    match.find();
    addChildWidgets(match.group(1));
  }

  public String render() throws Exception {
    StringBuffer html = new StringBuffer("<b>");
    html.append(childHtml).append("</b>");
    return html.toString()
  }
}

  

  • 垂直方向的靠近

若是說空白行隔離了概念,那麼代碼行的靠近便暗示了它們之間的緊密聯繫。因此,緊密相關的代碼應該互相靠近。

曾經的苦惱:在某個類中搜索,從一個函數調到另外一個函數,上下求索,想要弄清函數之間如何協做,最後卻被搞的摸不清頭腦,真是傷神。而想要了解系統作什麼,就須要花時間和經歷記住那些代碼碎片在哪裏。

因此,建議關係密切的概念互相靠近,雖然這條規則不適用分佈在不一樣文件中的概念,但在設計書寫時儘量地別把關係密切的概念放在不一樣的文件中。

  1. 變聲聲明應儘量靠近其使用的位置,函數很短,本地變量應該在函數的頂部出現
  2. 實體變量應該在類的頂部聲明
  3. 相關函數放在一塊兒,調用者儘量放在被調用者上面
  4. 概念相關的代碼應該放在一塊兒,相關性越強,彼此之間的距離就該越短
  • 垂直順序

閱讀通常是從上往下的順序,因此,編寫的時候也應該自上而下展現函數調用一來順序。也就是說,被調用函數應該放在執行調用的函數下面。

橫向格式

  • 水平方向的區隔和靠近

因運算符有兩個肯定而重要的的要素,左邊和右邊,其兩邊的空格增強了分隔效果,而函數後面的括號沒有加空格也是爲了強調其緊密關係。

private void measureLine(String line) {
  lineCount++;
  int lineSize = line.length()
  totalChars += lineSize;
  lineWidthHistogram.addLine(lineSize, lineCount);
  recordWidestLinet(lineSize)
}

  

  • 水平對齊

以下代碼所示的對齊方式,像是在強調不重要的東西,把閱讀者的目光從真正意義上拉開。
往往閱讀到此處,總會不由自主的縱向閱讀,然,縱向的數據並無什麼關聯,咱們更須要了解知道的是,橫向的數據之間是怎樣的結果。再者,格式化工具還會自動消除這類對齊。
因此,儘可能不要爲了表面上的美觀影響到真實的閱讀感覺,甚至處理格式化的不一致。

public function validator(Request $request)
{
    $this->validate($request, [
        'user_id'       => 'required|integer|min:1',
        'page_size'     => 'integer|between:1,100',
        'page'          => 'integer|min:1',
        'filter'        => [
            'sometimes',
            'filterJson' => [
                ['field' => 'creative_name', 'operator' => ['EQUALS', 'CONTAINS'], 'value' => 'string'],
                ['field' => 'ad_type_id', 'operator' => ['EQUALS'], 'value' => 'integer']
            ]
        ],
        'order_by'      => [
            'sometimes',
            'orderByJson' => [
                'sort_field' => 'required|in:add_time',
                'sort_order' => 'required|in:ASC,DESC'
            ]
        ],
    ]);
}

  

無對齊寫法,

public function validator(Request $request)
{
    $this->validate($request, [
        'user_id' => 'required|integer|min:1',
        'page_size' => 'integer|between:1,100',
        'page' => 'integer|min:1',
        'filter' => [
            'sometimes',
            'filterJson' => [
                ['field' => 'creative_name', 'operator' => ['EQUALS', 'CONTAINS'], 'value' => 'string'],
                ['field' => 'ad_type_id', 'operator' => ['EQUALS'], 'value' => 'integer']
            ]
        ],
        'order_by' => [
            'sometimes',
            'orderByJson' => [
                'sort_field' => 'required|in:add_time',
                'sort_order' => 'required|in:ASC,DESC'
            ]
        ],
    ]);
}

  

團隊規則

每一個程序員都有本身喜歡的格式規則,但若是在一個團隊中工做,就是團隊說了算。
既是團隊,一組開發者應當認同一種格式風格,每一個人都採用那種風格。別讓閱讀的人以爲項目代碼是由一大票意見沒法統一的程序員寫的。

對象數據結構

將私有變量設爲private有一個理由,咱們不想其餘人依賴這些變量,任意操控這些變量,可在有些程序裏,卻出現了各類各樣的取值器和賦值器,私有變量被公之於衆,最後如同自己就是共有變量通常。
根據得墨忒耳律,模塊不該該瞭解它所操做的對象(或類)的內部狀況,根據暴露出的方法完成業務需求,若是實在須要改變,能夠選擇調整類或繼承類。

單元測試

TDD三定律,

  • 在編寫不能經過的單元測試前,不可能編寫生產代碼。
  • 只可編寫恰好沒法經過的單元測試,不能編譯也算不經過。
  • 只可編寫恰好足以經過當前失敗測試的生產代碼。

測試代碼也會愈來愈多,定不要不看重測試代碼,要像生產代碼同樣對待,不然當測試代碼混亂不堪的時候,前面所作的一切都白費了,沒有人願意再去管理這龐大而混亂的測試代碼。因此,測試代碼和生產代碼同樣重要,不是二等公民,須要被思考、被設計和被照料,像生產代碼同樣保持整潔。

整潔的測試須要遵循的五條規則 F.I.R.S.T,快速(Fast),測試運行速度須要夠快。運行慢了,就不肯意常常運行測試,就不能及時發現問題。獨立(Independent),測試應該相互獨立。相互影響的測試,可能由於上一級測試不經過致使後面一連串的不經過。可重複(Repeatable),測試應當在任何環境中重複經過。 Not repeat code, but repeat environment.自足驗證(Sele-Validating),測試應該有布爾值輸出。不管經過或失敗,不該該經過查看日誌來確認測試是否經過。有了現代單元測試工具,此項無需擔憂。及時(Timely),測試應及時編寫。即單元測試剛巧在使其經過的生產代碼以前編寫,由於生產代碼以後編寫很容易形成生成代碼難以測試,不知道咋寫。

相關文章
相關標籤/搜索