函數
函數
##1. 函數參數(最好少於2個)
限制函數參數個數極為重要,這樣測試你的函數容易點。有超過3個可選參數參數導致一個爆炸性組合成長,你會有成噸獨立參數情形要測試。 無參數是理想情況。 1個或2個都可以,最好避免3個。再多就需要加固了。通常如果你的函數有超過兩個參數,表示他要處理的事太多了。如果必須要傳入很多數據,建議封裝一個高等級物件作為參數。壞:
function createMenu(string $title, string $body, string $buttonText, bool $cancellable): void { // ... }
好:
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 { // ... }
2. 函數應該只做一件事
這是迄今為止軟體工程裡最重要的一個規則。當一個函數做超過一件事的時候,他們就難於實現、測試和理解。當你把一個函數拆分到只剩下一個功能時,他們就容易被重構,然後你的程式碼讀起來就更清楚。如果你光遵循這條規則,你就領先於大多數開發者了。壞:
function emailClients(array $clients): void { foreach ($clients as $client) { $clientRecord = $db->find($client); if ($clientRecord->isActive()) { email($client); } } }
好:
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(); }
3. 函數名稱應體現他做了什麼事
壞: class Email
{
//...
public function handle(): void
{
mail($this->to, $this->subject, $this->body);
}
}
$message = new Email(...);
// 啥?handle处理一个消息干嘛了?是往一个文件里写吗?
$message->handle();
好:
class Email { //... public function send(): void { mail($this->to, $this->subject, $this->body); } } $message = new Email(...); // 简单明了 $message->send();
4. 函數裡應該只有一層抽象abstraction
當你抽象層次過多時,函數處理的事情太多了。需要拆分功能來提高可重複使用性和易用性,以便簡化測試。 (譯者註:這裡從範例程式碼看應該是指嵌套過多)壞:
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()方法還是很複雜,而且不利於測試。
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()方法的依賴移除。
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()
運行是否符合預期。
5. 不要用flag當函數的參數
flag就是在告訴大家,這個方法處理很多事。前面剛說過,一個函數要只做一件事。把不同flag的程式碼拆分到多個函數裡。
壞:
function createFile(string $name, bool $temp = false): void { if ($temp) { touch('./temp/'.$name); } else { touch($name); } } 好: function createFile(string $name): void { touch($name); } function createTempFile(string $name): void { touch('./temp/'.$name); }
避免副作用
一個函數做了比取得一個值然後傳回另外一個值或值們會產生副作用如果。副作用可能是寫入一個文件,修改某些全域變數或偶然的把你全部的錢給了陌生人。
現在,你的確需要在一個程式或場合裡要有副作用,像之前的例子,你也許需要寫一個檔案。你想要做的是把你做這些的地方集中起來。不要用幾個函數和類別來寫入一個特定的檔案。用一個服務來做它,一個只有一個。
重點是避免常見陷阱例如物件間共享無結構的數據,使用可以寫入任何可變的資料類型,不集中處理副作用發生的地方。如果你做了這些你就會比大多數程式設計師快樂。
壞:
// 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'];
好:
function splitIntoFirstAndLastName(string $name): array { return explode(' ', $name); } $name = 'Ryan McDermott'; $newName = splitIntoFirstAndLastName($name); var_dump($name); // 'Ryan McDermott'; var_dump($newName); // ['Ryan', 'McDermott'];
6. 不要寫全域函數
在大多數語言中污染全域變數是一個壞的實踐,因為你可能和其他類別庫衝突並且呼叫你api的人直到他們捕獲異常才知道踩坑了。讓我們思考一個場景: 如果你想配置一個數組,你可能會寫一個全域函數config()
,但是他可能 和試著做同樣事的其他類別庫衝突。
壞:
function config(): array { return [ 'foo' => 'bar', ] }
好:
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 类的实例 $configuration = new Configuration([ 'foo' => 'bar', ]);
現在你必須在程式中用Configuration
的實例了
#7. 不要使用單例模式
單例是一種反模式. 以下是解釋:Paraphrased from Brian Button:
總是被用成全域實例。 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 themough the interthings. Making something gface meas oid gface smell passing sm
- #違反了單一回應原則They violate the single responsibility principle: by virtue of the fact that they control their own creation and lifecycle.
- 導致程式碼強耦合They inherently cause code to be tightly coupled. This makes faking them out under test rather difficult in many cases.
- 在整個程式的生命週期中始終攜帶狀態。 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 testuse big noother the tests. .
這裡有一篇非常好的討論單例模式的[根本問題((http://misko.hevery.com/2008/08/25/root-cause-of-singletons/)的文章,是Misko Hevery 寫的。
壞:
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();
好:##
class DBConnection { public function __construct(string $dsn) { // ... } // ... }建立
DBConnection 類別的實例並透過DSN 設定.
$connection = new DBConnection($dsn);
DBConnection 的實例了
#8.封裝條件語句
壞:
#
if ($article->state === 'published') { // ... }好:
if ($article->isPublished()) { // ... }
#9.避免用反義條件判斷壞:
function isDOMNodeNotPresent(\DOMNode $node): bool { // ... } if (!isDOMNodeNotPresent($node)) { // ... }
好:
function isDOMNodePresent(\DOMNode $node): bool { // ... } if (isDOMNodePresent($node)) { // ... }
10. 避免條件判斷
這看起來像是一個不可能任務。當人們第一次聽到這句話是都會這麼說。" 沒有if語句我還能做啥?" 答案是你可以使用多態來實現多種場景的相同任務。第二個問題很常見, “這麼做可以,但為什麼我要這麼做?” 答案是前面我們學過的一個 Clean Code原則:一個函數應該只做一件事。當你有很多含有if語句的類別和函數時,你的函數做了不只一件事。記住,只做一件事。#壞:
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(); } } }
#好:
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(); } }
11. 避免型別檢查(part 1)
PHP是弱型別的,這表示你的函數可以接收任何類型的參數。有時候你為這自由所痛苦並且在你的函數漸漸嘗試類型檢查。有很多方法去避免這麼做。第一種是統一API。壞:
function travelToTexas($vehicle): void { if ($vehicle instanceof Bicycle) { $vehicle->pedalTo(new Location('texas')); } elseif ($vehicle instanceof Car) { $vehicle->driveTo(new Location('texas')); } }
好:
function travelToTexas(Vehicle $vehicle): void { $vehicle->travelTo(new Location('texas')); }
#12. 避免型別檢查(part 2)
#如果你正使用基本原始值例如字串、整形和數組,要求版本是PHP 7 ,不用多態,需要類型檢測, 那你應考慮類型聲明或嚴格模式。提供了基於標準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; }
好:
function combine(int $val1, int $val2): int { return $val1 + $val2; }
13. 移除殭屍程式碼
######殭屍程式碼和重複程式碼一樣壞。沒有理由保留在你的程式碼庫中。如果從來沒被呼叫過,就刪掉!因為還在程式碼版本庫裡,因此很安全。 #########壞:######function oldRequestModule(string $url): void { // ... } function newRequestModule(string $url): void { // ... } $request = newRequestModule($requestUrl); inventoryTracker('apples', $request, 'www.inventory-awesome.io');######好:######
function requestModule(string $url): void { // ... } $request = requestModule($requestUrl); inventoryTracker('apples', $request, 'www.inventory-awesome.io');