## **函數**
### 函數參數(最好少于2個)
限制函數參數的數量極其重要,這樣測試你的函數容易點。有超過3個可選參數參數導致一個爆炸式組合增長,你會有成噸獨立參數情形要測試。
無參數是理想情況,1個或2個都可以,最好避免3個,再多就需要作處理了。如果函數存在超過兩個參數,說明它要處理的事太多了。 如果必須要傳入很多的參數數據,建議封裝一個高級別的對象作為參數。
**差:**
```php
function createMenu(string $title, string $body, string $buttonText, bool $cancellable): void
{
// ...
}
```
**優:**
```php
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): void
{
// ...
}
```
### 函數應該只做一件事
這是迄今為止軟件工程里最重要的一個規則。當一個函數做超過一件事的時候,他們就難于實現、測試和理解。當你把一個函數拆分到只剩一個功能時,他們就容易被重構,然后你的代碼讀起來就更清晰。如果你光遵循這條規則,你就領先于大多數開發者了。
**差:**
```php
function emailClients(array $clients): void
{
foreach ($clients as $client) {
$clientRecord = $db->find($client);
if ($clientRecord->isActive()) {
email($client);
}
}
}
```
**優:**
```php
function emailClients(array $clients): void
{
$activeClients = activeClients($clients);
array_walk($activeClients, 'email');
}
function activeClients(array $clients): array
{
return array_filter($clients, 'isClientActive');
}
function isClientActive(int $client): bool
{
$clientRecord = $db->find($client);
return $clientRecord->isActive();
}
```
### 函數名應該是有意義的動詞(或表明具體做了什么事)
**差:**
```php
class Email
{
//...
public function handle(): void
{
mail($this->to, $this->subject, $this->body);
}
}
$message = new Email(...);
// 啥?handle處理一個消息干嘛了?是往一個文件里寫碼?
$message->handle();
```
**優:**
```php
class Email
{
//...
public function send(): void
{
mail($this->to, $this->subject, $this->body);
}
}
$message = new Email(...);
// 簡單明了
$message->send();
```
### 函數里應當只有一層抽象abstraction
當你抽象層次過多時時,函數處理的事情太多了。需要拆分功能來提高可重用性和易用性,以便簡化測試。
(譯者注:這里從示例代碼看應該是指嵌套過多)
**差:**
```php
function parseBetterJSAlternative(string $code): void
{
$regexes = [
// ...
];
$statements = explode(' ', $code);
$tokens = [];
foreach ($regexes as $regex) {
foreach ($statements as $statement) {
// ...
}
}
$ast = [];
foreach ($tokens as $token) {
// lex...
}
foreach ($ast as $node) {
// parse...
}
}
```
**差:**
我們把一些方法從循環中提取出來,但是`parseBetterJSAlternative()`方法還是很復雜,而且不利于測試。
```php
function tokenize(string $code): array
{
$regexes = [
// ...
];
$statements = explode(' ', $code);
$tokens = [];
foreach ($regexes as $regex) {
foreach ($statements as $statement) {
$tokens[] = /* ... */;
}
}
return $tokens;
}
function lexer(array $tokens): array
{
$ast = [];
foreach ($tokens as $token) {
$ast[] = /* ... */;
}
return $ast;
}
function parseBetterJSAlternative(string $code): void
{
$tokens = tokenize($code);
$ast = lexer($tokens);
foreach ($ast as $node) {
// 解析邏輯...
}
}
```
**優:**
最好的解決方案是把 `parseBetterJSAlternative()`方法的依賴移除。
```php
class Tokenizer
{
public function tokenize(string $code): array
{
$regexes = [
// ...
];
$statements = explode(' ', $code);
$tokens = [];
foreach ($regexes as $regex) {
foreach ($statements as $statement) {
$tokens[] = /* ... */;
}
}
return $tokens;
}
}
class Lexer
{
public function lexify(array $tokens): array
{
$ast = [];
foreach ($tokens as $token) {
$ast[] = /* ... */;
}
return $ast;
}
}
class BetterJSAlternative
{
private $tokenizer;
private $lexer;
public function __construct(Tokenizer $tokenizer, Lexer $lexer)
{
$this->tokenizer = $tokenizer;
$this->lexer = $lexer;
}
public function parse(string $code): void
{
$tokens = $this->tokenizer->tokenize($code);
$ast = $this->lexer->lexify($tokens);
foreach ($ast as $node) {
// 解析邏輯...
}
}
}
```
這樣我們可以對依賴做mock,并測試`BetterJSAlternative::parse()`運行是否符合預期。
### 不要用flag作為函數的參數
flag就是在告訴大家,這個方法里處理很多事。前面剛說過,一個函數應當只做一件事。 把不同flag的代碼拆分到多個函數里。
**差:**
```php
function createFile(string $name, bool $temp = false): void
{
if ($temp) {
touch('./temp/'.$name);
} else {
touch($name);
}
}
```
**優:**
```php
function createFile(string $name): void
{
touch($name);
}
function createTempFile(string $name): void
{
touch('./temp/'.$name);
}
```
### 避免副作用
一個函數做了比獲取一個值然后返回另外一個值或值們會產生副作用。副作用可能是寫入一個文件,修改某些全局變量或者偶然的把你全部的錢給了陌生人。
現在,你的確需要在一個程序或者場合里要有副作用,像之前的例子,你也許需要寫一個文件。你想要做的是把你做這些的地方集中起來。不要用幾個函數和類來寫入一個特定的文件。用一個服務來做它,一個只有一個。
重點是避免常見陷阱比如對象間共享無結構的數據,使用可以寫入任何的可變數據類型,不集中處理副作用發生的地方。如果你做了這些你就會比大多數程序員快樂。
**差:**
```php
// 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(): void
{
global $name;
$name = explode(' ', $name);
}
splitIntoFirstAndLastName();
var_dump($name); // ['Ryan', 'McDermott'];
```
**優:**
```php
function splitIntoFirstAndLastName(string $name): array
{
return explode(' ', $name);
}
$name = 'Ryan McDermott';
$newName = splitIntoFirstAndLastName($name);
var_dump($name); // 'Ryan McDermott';
var_dump($newName); // ['Ryan', 'McDermott'];
```
### 不要寫全局函數
在大多數語言中污染全局變量是一個壞的實踐,因為你可能和其他類庫沖突
并且調用你api的人直到他們捕獲異常才知道踩坑了。讓我們思考一種場景:
如果你想配置一個數組,你可能會寫一個全局函數`config()`,但是他可能
和試著做同樣事的其他類庫沖突。
**差:**
```php
function config(): array
{
return [
'foo' => 'bar',
]
}
```
**優:**
```php
class Configuration
{
private $configuration = [];
public function __construct(array $configuration)
{
$this->configuration = $configuration;
}
public function get(string $key): ?string
{
return isset($this->configuration[$key]) ? $this->configuration[$key] : null;
}
}
```
加載配置并創建 `Configuration` 類的實例
```php
$configuration = new Configuration([
'foo' => 'bar',
]);
```
現在你必須在程序中用 `Configuration` 的實例了
### 不要使用單例模式
單例是一種 [反模式](https://en.wikipedia.org/wiki/Singleton_pattern). 以下是解釋:Paraphrased from Brian Button:
1. 總是被用成全局實例。They are generally used as a **global instance**, why is that so bad? Because **you hide the dependencies** of your application in your code, instead of exposing them through the interfaces. Making something global to avoid passing it around is a [code smell](https://en.wikipedia.org/wiki/Code_smell).
2. 違反了[單一響應原則]()They violate the [single responsibility principle](#single-responsibility-principle-srp): by virtue of the fact that **they control their own creation and lifecycle**.
3. 導致代碼強耦合They inherently cause code to be tightly [coupled](https://en.wikipedia.org/wiki/Coupling_%28computer_programming%29). This makes faking them out under **test rather difficult** in many cases.
4. 在整個程序的生命周期中始終攜帶狀態。They carry state around for the lifetime of the application. Another hit to testing since **you can end up with a situation where tests need to be ordered** which is a big no for unit tests. Why? Because each unit test should be independent from the other.
**差:**
```php
class DBConnection
{
private static $instance;
private function __construct(string $dsn)
{
// ...
}
public static function getInstance(): DBConnection
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
// ...
}
$singleton = DBConnection::getInstance();
```
**優:**
```php
class DBConnection
{
public function __construct(string $dsn)
{
// ...
}
// ...
}
```
創建 `DBConnection` 類的實例并通過 [DSN](http://php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters) 配置.
```php
$connection = new DBConnection($dsn);
```
現在你必須在程序中 使用 `DBConnection` 的實例了
### 封裝條件語句
**差:**
```php
if ($article->state === 'published') {
// ...
}
```
**優:**
```php
if ($article->isPublished()) {
// ...
}
```
### 避免用反義條件判斷
**差:**
```php
function isDOMNodeNotPresent(\DOMNode $node): bool
{
// ...
}
if (!isDOMNodeNotPresent($node))
{
// ...
}
```
**優:**
```php
function isDOMNodePresent(\DOMNode $node): bool
{
// ...
}
if (isDOMNodePresent($node)) {
// ...
}
```
### 避免條件判斷
這看起來像一個不可能任務。當人們第一次聽到這句話是都會這么說。
"沒有`if語句`我還能做啥?" 答案是你可以使用多態來實現多種場景
的相同任務。第二個問題很常見, “這么做可以,但為什么我要這么做?”
答案是前面我們學過的一個Clean Code原則:一個函數應當只做一件事。
當你有很多含有`if`語句的類和函數時,你的函數做了不止一件事。
記住,只做一件事。
**差:**
```php
class Airplane
{
// ...
public function getCruisingAltitude(): int
{
switch ($this->type) {
case '777':
return $this->getMaxAltitude() - $this->getPassengerCount();
case 'Air Force One':
return $this->getMaxAltitude();
case 'Cessna':
return $this->getMaxAltitude() - $this->getFuelExpenditure();
}
}
}
```
**優:**
```php
interface Airplane
{
// ...
public function getCruisingAltitude(): int;
}
class Boeing777 implements Airplane
{
// ...
public function getCruisingAltitude(): int
{
return $this->getMaxAltitude() - $this->getPassengerCount();
}
}
class AirForceOne implements Airplane
{
// ...
public function getCruisingAltitude(): int
{
return $this->getMaxAltitude();
}
}
class Cessna implements Airplane
{
// ...
public function getCruisingAltitude(): int
{
return $this->getMaxAltitude() - $this->getFuelExpenditure();
}
}
```
### 避免類型檢查 (part 1)
PHP是弱類型的,這意味著你的函數可以接收任何類型的參數。
有時候你為這自由所痛苦并且在你的函數漸漸嘗試類型檢查。
有很多方法去避免這么做。第一種是統一API。
**差:**
```php
function travelToTexas($vehicle): void
{
if ($vehicle instanceof Bicycle) {
$vehicle->pedalTo(new Location('texas'));
} elseif ($vehicle instanceof Car) {
$vehicle->driveTo(new Location('texas'));
}
}
```
**優:**
```php
function travelToTexas(Traveler $vehicle): void
{
$vehicle->travelTo(new Location('texas'));
}
```
### 避免類型檢查 (part 2)
如果你正使用基本原始值比如字符串、整形和數組,要求版本是PHP 7+,不用多態,需要類型檢測,
那你應當考慮[類型聲明](http://php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration)或者嚴格模式。
提供了基于標準PHP語法的靜態類型。 手動檢查類型的問題是做好了需要好多的廢話,好像為了安全就可以不顧損失可讀性。
保持你的PHP 整潔,寫好測試,做好代碼回顧。做不到就用PHP嚴格類型聲明和嚴格模式來確保安全。
**差:**
```php
function combine($val1, $val2): int
{
if (!is_numeric($val1) || !is_numeric($val2)) {
throw new \Exception('Must be of type Number');
}
return $val1 + $val2;
}
```
**優:**
```php
function combine(int $val1, int $val2): int
{
return $val1 + $val2;
}
```