PHP 整潔之道

王子昊發表於2019-08-13

Table of Contents

  1. 介紹
  2. 變數
  3. 函式

介紹

本文由 yangweijie 翻譯自clen php code,團建用,歡迎大家指正。

摘錄自 Robert C. Martin的Clean Code 書中的軟體工程師的原則
,適用於PHP。 這不是風格指南。 這是一個關於開發可讀、可複用並且可重構的PHP軟體指南。

並不是這裡所有的原則都得遵循,甚至很少的能被普遍接受。 這些雖然只是指導,但是都是Clean Code作者多年總結出來的。

Inspired from clean-code-javascript

變數

使用有意義且可拼寫的變數名

Bad:

$ymdstr = $moment->format('y-m-d');

Good:

$currentDate = $moment->format('y-m-d');

同種型別的變數使用相同詞彙

Bad:

getUserInfo();
getClientData();
getCustomerRecord();

Good:

getUser();

使用易檢索的名稱

我們會讀比寫要多的程式碼。通過是命名易搜尋,讓我們寫出可讀性和易搜尋程式碼很重要。

Bad:

// What the heck is 86400 for?
addExpireAt(86400);

Good:

// Declare them as capitalized `const` globals.
interface DateGlobal {
  const SECONDS_IN_A_DAY = 86400;
}

addExpireAt(DateGlobal::SECONDS_IN_A_DAY);

使用解釋型變數

Bad:

$address = 'One Infinite Loop, Cupertino 95014';
$cityZipCodeRegex = '/^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/';
preg_match($cityZipCodeRegex, $address, $matches);
saveCityZipCode($matches[1], $matches[2]);

Good:

$address = 'One Infinite Loop, Cupertino 95014';
$cityZipCodeRegex = '/^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/';
preg_match($cityZipCodeRegex, $address, $matches);
list(, $city, $zipCode) = $matchers;
saveCityZipCode($city, $zipCode);

避免心理對映

明確比隱性好。

Bad:

$l = ['Austin', 'New York', 'San Francisco'];
for($i=0; $i<count($l); $i++) {
  oStuff();
  doSomeOtherStuff();
  // ...
  // ...
  // ...
  // 等等`$l` 又代表什麼?
  dispatch($l);
}

Good:

$locations = ['Austin', 'New York', 'San Francisco'];
for($i=0; $i<count($locations); $i++) {
  $location = $locations[$i];

  doStuff();
  doSomeOtherStuff();
  // ...
  // ...
  // ...
  dispatch($location);
});

不要新增不必要上下文

如果你的class/object 名能告訴你什麼,不要把它重複在你變數名裡。

Bad:

$car = [
  'carMake'  => 'Honda',
  'carModel' => 'Accord',
  'carColor' => 'Blue',
];

function paintCar(&$car) {
  $car['carColor'] = 'Red';
}

Good:

$car = [
  'make'  => 'Honda',
  'model' => 'Accord',
  'color' => 'Blue',
];

function paintCar(&$car) {
  $car['color'] = 'Red';
}

使用引數預設值代替短路或條件語句。

Bad:

function createMicrobrewery($name = null) {
  $breweryName = $name ?: 'Hipster Brew Co.';
  // ...
}

Good:

function createMicrobrewery($breweryName = 'Hipster Brew Co.') {
  // ...
}

函式

函式引數最好少於2個

限制函式引數個數極其重要因為它是你函式測試容易點。有超過3個可選引數引數導致一個爆炸式組合增長,你會有成噸獨立引數情形要測試。

無引數是理想情況。1個或2個都可以,最好避免3個。再多就需要加固了。通常如果你的函式有超過兩個引數,說明他多做了一些事。 在引數多的情況裡,大多數時候一個高階別物件(陣列)作為引數就足夠應付。

Bad:

function createMenu($title, $body, $buttonText, $cancellable) {
  // ...
}

Good:

class menuConfig() {
  public $title;
  public $body;
  public $buttonText;
  public $cancellable = false;
}

$config = new MenuConfig();
$config->title = 'Foo';
$config->body = 'Bar';
$config->buttonText = 'Baz';
$config->cancellable = true;

function createMenu(MenuConfig $config) {
  // ...
}

函式應該只做一件事

這是迄今為止軟體工程裡最重要的一個規則。當函式做超過一件事的時候,他們就難於實現、測試和理解。當你隔離函式只剩一個功能時,他們就容易被重構,然後你的程式碼讀起來就更清晰。如果你光遵循這條規則,你就領先於大多數開發者了。

Bad:

function emailClients($clients) {
  foreach ($clients as $client) {
    $clientRecord = $db->find($client);
    if($clientRecord->isActive()) {
       email($client);
    }
  }
}

Good:

function emailClients($clients) {
  $activeClients = activeClients($clients);
  array_walk($activeClients, 'email');
}

function activeClients($clients) {
  return array_filter($clients, 'isClientActive');
}

function isClientActive($client) {
  $clientRecord = $db->find($client);
  return $clientRecord->isActive();
}

函式名應當描述他們所做的事

Bad:

function addToDate($date, $month) {
  // ...
}

$date = new \DateTime();

// It's hard to to tell from the function name what is added
addToDate($date, 1);

Good:

function addMonthToDate($month, $date) {
  // ...
}

$date = new \DateTime();
addMonthToDate(1, $date);

函式應當只為一層抽象,當你超過一層抽象時,函式正在做多件事。拆分功能易達到可重用性和易用性。.

Bad:

function parseBetterJSAlternative($code) {
  $regexes = [
    // ...
  ];

  $statements = split(' ', $code);
  $tokens = [];
  foreach($regexes as $regex) {
    foreach($statements as $statement) {
      // ...
    }
  }

  $ast = [];
  foreach($tokens as $token) {
     // lex...
  }

  foreach($ast as $node) {
   // parse...
  }
}

Good:

function tokenize($code) {
  $regexes = [
    // ...
  ];

  $statements = split(' ', $code);
  $tokens = [];
  foreach($regexes as $regex) {
    foreach($statements as $statement) {
      $tokens[] = /* ... */;
    });
  });

  return tokens;
}

function lexer($tokens) {
  $ast = [];
  foreach($tokens as $token) {
    $ast[] = /* ... */;
  });

  return ast;
}

function parseBetterJSAlternative($code) {
  $tokens = tokenize($code);
  $ast = lexer($tokens);
  foreach($ast as $node) {
    // parse...
  });
}

刪除重複的程式碼

盡你最大的努力來避免重複的程式碼。重複程式碼不好,因為它意味著如果你修改一些邏輯,那就有不止一處地方要同步修改了。

想象一下如果你經營著一家餐廳並跟蹤它的庫存: 你全部的西紅柿、洋蔥、大蒜、香料等。如果你保留有多個列表,當你服務一個有著西紅柿的菜,那麼所有記錄都得更新。如果你只有一個列表,那麼只需要修改一個地方!

經常你容忍重複程式碼,因為你有兩個或更多有共同部分但是少許差異的東西強制你用兩個或更多獨立的函式來做相同的事。移除重複程式碼意味著創造一個處理這組不同事物的一個抽象,只需要一個函式/模組/類。

抽象正確非常重要,這也是為什麼你應當遵循SOLID原則(奠定Class基礎的原則)。壞的抽象可能比重複程式碼還要糟,因為要小心。在這個前提下,如果你可以抽象好,那就開始做把!不要重複你自己,否則任何你想改變一件事的時候你都發現在即在更新維護多處。

Bad:

function showDeveloperList($developers) {
  foreach($developers as $developer) {
    $expectedSalary = $developer->calculateExpectedSalary();
    $experience = $developer->getExperience();
    $githubLink = $developer->getGithubLink();
    $data = [
      $expectedSalary,
      $experience,
      $githubLink
    ];

    render($data);
  }
}

function showManagerList($managers) {
  foreach($managers as $manager) {
    $expectedSalary = $manager->calculateExpectedSalary();
    $experience = $manager->getExperience();
    $githubLink = $manager->getGithubLink();
    $data = [
      $expectedSalary,
      $experience,
      $githubLink
    ];

    render($data);
  }
}

Good:

function showList($employees) {
  foreach($employees as $employe) {
    $expectedSalary = $employe->calculateExpectedSalary();
    $experience = $employe->getExperience();
    $githubLink = $employe->getGithubLink();
    $data = [
      $expectedSalary,
      $experience,
      $githubLink
    ];

    render($data);
  }
}

通過物件賦值設定預設值

Bad:

$menuConfig = [
  'title'       => null,
  'body'        => 'Bar',
  'buttonText'  => null,
  'cancellable' => true,
];

function createMenu(&$config) {
  $config['title']       = $config['title'] ?: 'Foo';
  $config['body']        = $config['body'] ?: 'Bar';
  $config['buttonText']  = $config['buttonText'] ?: 'Baz';
  $config['cancellable'] = $config['cancellable'] ?: true;
}

createMenu($menuConfig);

Good:

$menuConfig = [
  'title'       => 'Order',
  // User did not include 'body' key
  'buttonText'  => 'Send',
  'cancellable' => true,
];

function createMenu(&$config) {
  $config = array_merge([
    'title'       => 'Foo',
    'body'        => 'Bar',
    'buttonText'  => 'Baz',
    'cancellable' => true,
  ], $config);

  // config now equals: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
  // ...
}

createMenu($menuConfig);

不要用標誌作為函式的引數,標誌告訴你的使用者函式做很多事了。函式應當只做一件事。 根據布林值區別的路徑來拆分你的複雜函式。

Bad:

function createFile(name, temp = false) {
  if (temp) {
    touch('./temp/'.$name);
  } else {
    touch($name);
  }
}

Good:

function createFile($name) {
  touch(name);
}

function createTempFile($name) {
  touch('./temp/'.$name);
}

避免副作用

一個函式做了比獲取一個值然後返回另外一個值或值們會產生副作用如果。副作用可能是寫入一個檔案,修改某些全域性變數或者偶然的把你全部的錢給了陌生人。

現在,你的確需要在一個程式或者場合裡要有副作用,像之前的例子,你也許需要寫一個檔案。你想要做的是把你做這些的地方集中起來。不要用幾個函式和類來寫入一個特定的檔案。用一個服務來做它,一個只有一個。

重點是避免常見陷阱比如物件間共享無結構的資料,使用可以寫入任何的可變資料型別,不集中處理副作用發生的地方。如果你做了這些你就會比大多數程式設計師快樂。

Bad:

// Global variable referenced by following function.
// If we had another function that used this name, now it'd be an array and it could break it.
$name = 'Ryan McDermott';

function splitIntoFirstAndLastName() {
  $name = preg_split('/ /', $name);
}

splitIntoFirstAndLastName();

var_dump($name); // ['Ryan', 'McDermott'];

Good:

$name = 'Ryan McDermott';

function splitIntoFirstAndLastName($name) {
  return preg_split('/ /', $name);
}

$name = 'Ryan McDermott';
$newName = splitIntoFirstAndLastName($name);

var_export($name); // 'Ryan McDermott';
var_export($newName); // ['Ryan', 'McDermott'];

不要寫全域性函式

在大多數語言中汙染全域性變數是一個壞的實踐,因為你可能和其他類庫衝突並且你api的使用者不明白為什麼直到他們獲得產品的一個異常。讓我們看一個例子:如果你想配置一個陣列,你可能會寫一個全域性函式像config(),但是可能和試著做同樣事的其他類庫衝突。這就是為什麼單例設計模式和簡單配置會更好的原因。

Bad:

function config() {
  return  [
    'foo': 'bar',
  ]
};

Good:

class Configuration {
  private static $instance;
  private function __construct($configuration) {/* */}
  public static function getInstance() {
     if(self::$instance === null) {
         self::$instance = new Configuration();
     }
     return self::$instance;
 }
 public function get($key) {/* */}
 public function getAll() {/* */}
}

$singleton = Configuration::getInstance();

封裝條件語句

Bad:

if ($fsm->state === 'fetching' && is_empty($listNode)) {
  // ...
}

Good:

function shouldShowSpinner($fsm, $listNode) {
  return $fsm->state === 'fetching' && is_empty(listNode);
}

if (shouldShowSpinner($fsmInstance, $listNodeInstance)) {
  // ...
}

避免消極條件

Bad:

function isDOMNodeNotPresent($node) {
  // ...
}

if (!isDOMNodeNotPresent($node)) {
  // ...
}

Good:

function isDOMNodePresent($node) {
  // ...
}

if (isDOMNodePresent($node)) {
  // ...
}

避免條件宣告

這看起來像一個不可能任務。當人們第一次聽到這句話是都會這麼說。
"沒有一個if宣告" 答案是你可以使用多型來達到許多case語句裡的任務。第二個問題很常見, “那麼為什麼我要那麼做?” 答案是前面我們學過的一個整潔程式碼原則:一個函式應當只做一件事。當你有類和函式有很多if宣告,你自己知道你的函式做了不止一件事。記住,只做一件事。

Bad:

class Airplane {
  // ...
  public function getCruisingAltitude() {
    switch (this.type) {
      case '777':
        return $this->getMaxAltitude() - $this->getPassengerCount();
      case 'Air Force One':
        return $this->getMaxAltitude();
      case 'Cessna':
        return $this->getMaxAltitude() - $this->getFuelExpenditure();
    }
  }
}

Good:

class Airplane {
  // ...
}

class Boeing777 extends Airplane {
  // ...
  public function getCruisingAltitude() {
    return $this->getMaxAltitude() - $this->getPassengerCount();
  }
}

class AirForceOne extends Airplane {
  // ...
  public function getCruisingAltitude() {
    return $this->getMaxAltitude();
  }
}

class Cessna extends Airplane {
  // ...
  public function getCruisingAltitude() {
    return $this->getMaxAltitude() - $this->getFuelExpenditure();
  }
}

Avoid 避免型別檢查 (part 1)

PHP是弱型別的,這意味著你的函式可以接收任何型別的引數。
有時候你為這自由所痛苦並且在你的函式漸漸嘗試型別檢查。有很多方法去避免這麼做。第一種是考慮API的一致性。

Bad:

function travelToTexas($vehicle) {
  if ($vehicle instanceof Bicycle) {
    $vehicle->peddle($this->currentLocation, new Location('texas'));
  } else if ($vehicle instanceof Car) {
    $vehicle->drive($this->currentLocation, new Location('texas'));
  }
}

Good:

function travelToTexas($vehicle) {
  $vehicle->move($this->currentLocation, new Location('texas'));
}

避免型別檢查 (part 2)

如果你正使用基本原始值比如字串、整形和陣列,你不能用多型,你仍然感覺需要型別檢測,你應當考慮型別宣告或者嚴格模式。 這給你了基於標準PHP語法的靜態型別。 手動檢查型別的問題是做好了需要好多的廢話,好像為了安全就可以不顧損失可讀性。保持你的PHP 整潔,寫好測試,做好程式碼回顧。做不到就用PHP嚴格型別宣告和嚴格模式來確保安全。

Bad:

function combine($val1, $val2) {
  if (is_numeric($val1) && is_numeric(val2)) {
    return val1 + val2;
  }

  throw new \Exception('Must be of type Number');
}

Good:

function combine(int $val1, int $val2) {
  return $val1 + $val2;
}

移除殭屍程式碼

殭屍程式碼和重複程式碼一樣壞。沒有理由保留在你的程式碼庫中。如果從來被呼叫過,見鬼去!在你的版本庫裡是如果你仍然需要他的話,因此這麼做很安全。

Bad:

function oldRequestModule($url) {
  // ...
}

function newRequestModule($url) {
  // ...
}

$req = new newRequestModule();
inventoryTracker('apples', $req, 'www.inventory-awesome.io');

Good:

function newRequestModule($url) {
  // ...
}

$req = new newRequestModule();
inventoryTracker('apples', $req, 'www.inventory-awesome.io');

相關文章