重構 PHP 程式碼

Marden發表於2023-02-21

重構指的是在不改變原有功能的情況下,修改或者重新編寫程式碼。

下面的例子中,我將向你展示如何更好地編寫程式碼。

#1 - 表現力

這可能只是一個簡單的技巧,但編寫富有表現力的程式碼可以大大改進我們的程式碼。總是讓程式碼自我解釋,這樣未來的你或其他開發人員都能知道程式碼中發生了什麼。

不過也有開發人員表示,命名是程式設計中最困難的事情之一。這就是為什麼這不像聽起來那麼容易的原因之一。 ?‍

示例 #1 - 命名

之前

// ❌ 這個方法是用來做什麼的,方法名錶達並不清晰
// ❌ 是設定狀態還是檢查狀態呢?
$status = $user->status('pending');

之後

// ✅ 透過新增 is,使方法名錶達的意圖更清晰
// ✅ 檢測使用者狀態是否與給定狀態相等
// ✅ 同時新變數名讓我們可以推斷它是布林值
$isUserPending = $user->isStatus('pending');

示例 #2 - 命名

之前

// ❌ 這個類返回的是什麼?類名?類全名?還是類路徑?
return $factory->getTargetClass();

之後

// ✅ 我們獲取的是類路徑
// ✅ 如果使用者想要類名?則找錯了方法
return $factory->getTargetClassPath();

示例 #3 - 提取

之前

// ❌ 重複的程式碼 ( "file_get_contents", "base_path" 方法以及檔案擴充套件)
// ❌ 此刻,我們不去關心如何獲得code examples
public function setCodeExamples(string $exampleBefore, string $exampleAfter)
{
    $this->exampleBefore = file_get_contents(base_path("$exampleBefore.md"));
    $this->exampleAfter = file_get_contents(base_path("$exampleAfter.md"));
}

之後

public function setCodeExamples(string $exampleBefore, string $exampleAfter)
{ 
    // ✅ 程式碼直接說明了我們的意圖:獲取code example(不關注如何獲取)
    $this->exampleBefore = $this->getCodeExample($exampleBefore);
    $this->exampleAfter = $this->getCodeExample($exampleAfter);
}

// ✅ 這個新方法可多次呼叫
private function getCodeExample(string $exampleName): string
{
    return file_get_contents(base_path("$exampleName.md"));
}

示例 #4 - 提取

之前

// ❌ 多重 where 語句,使閱讀變得困難
// ❌ 意圖究竟是什麼呢?
User::whereNotNull('subscribed')->where('status', 'active');

之後

// ✅ 這個新的scope方法說明了發生了什麼事
// ✅ 如果我們需要了解更多細節,可以進入這個scope方法內部去了解
// ✅ "subscribed" scope 方法可在其他地方使用
User::subscribed();

示例 #5 - 提取

這是我之前專案的一個例子。我們用命令列匯入使用者。 ImportUsersCommand 類中含有一個handle 方法,用來處理任務。

之前

protected function handle()
{
    // ❌ 這個方法包含太多程式碼
    $url = $this->option('url') ?: $this->ask('Please provide the URL for the import:');

    $importResponse =  $this->http->get($url);

    // ❌ 進度條對使用者很有用,不過卻讓程式碼顯得雜亂
    $bar = $this->output->createProgressBar($importResponse->count());
    $bar->start();

    $this->userRepository->truncate();
    collect($importResponse->results)->each(function (array $attributes) use ($bar) {
        $this->userRepository->create($attributes);
        $bar->advance();
    });

    // ❌ 很難說清此處發生了哪些行為
    $bar->finish();
    $this->output->newLine();

    $this->info('Thanks. Users have been imported.');

    if($this->option('with-backup')) {
        $this->storage
            ->disk('backups')
            ->put(date('Y-m-d').'-import.json', $response->body());

        $this->info('Backup was stored successfully.');
    }

}

之後

protected function handle(): void
{
    // ✅ handle方法是你訪問該類首先會檢視的方法
    // ✅ 現在可以很容易就對這個方法做了些什麼有個粗略的瞭解
    $url = $this->option('url') ?: $this->ask('Please provide the URL for the import:');

    $importResponse =  $this->http->get($url);

    $this->importUsers($importResponse->results);

    $this->saveBackupIfAsked($importResponse);
}

// ✅ 如果需要了解更多細節,可以檢視這些專用的方法
protected function importUsers($userData): void
{
    $bar = $this->output->createProgressBar(count($userData));
    $bar->start();

    $this->userRepository->truncate();
    collect($userData)->each(function (array $attributes) use ($bar) {
        $this->userRepository->create($attributes);
        $bar->advance();
    });

    $bar->finish();
    $this->output->newLine();

    $this->info('Thanks. Users have been imported.');
}

// ✅ 不要害怕使用多行程式碼
// ✅ 這個例子中它讓我們核心的 handle 方法更為簡潔
protected function saveBackupIfAsked(Response $response): void
{
    if($this->option('with-backup')) {
        $this->storage
            ->disk('backups')
            ->put(date('Y-m-d').'-import.json', $response->body());

        $this->info('Backup was stored successfully.');
    }
}

#2 -提前返回

提前返回指的是,我們嘗試透過將結構分解為特定case來避免巢狀的做法。這樣,我們得到了更線性的程式碼,更易於閱讀和了解。不要害怕使用多個return語句。

示例 #1

之前

public function calculateScore(User $user): int
{
    if ($user->inactive) {
        $score = 0;
    } else {
        // ❌ 怎麼又有一個 "if"?
        if ($user->hasBonus) {
            $score = $user->score + $this->bonus;
        } else {
            // ❌ 由於存在多個層級,大費眼神 ? 
            $score = $user->score;
        }
    }

    return $score;
}

之後

public function calculateScore(User $user): int
{
    // ✅ 邊緣用例提前檢測
    if ($user->inactive) {
        return 0;
    }

    // ✅ 每個用例都有自己的程式碼塊,使得更容易跟進
    if ($user->hasBonus) {
        return $user->score + $this->bonus;
    }

    return $user->score;
}

示例 #2

之前

public function sendInvoice(Invoice $invoice): void
{
    if($user->notificationChannel === 'Slack')
    {
        $this->notifier->slack($invoice);
    } else {
        // ❌ 即使是簡單的ELSE都影響程式碼的可讀性
        $this->notifier->email($invoice);
    }
}

之後

public function sendInvoice(Invoice $invoice): bool
{
    // ✅ 每個條件都易讀
    if($user->notificationChannel === 'Slack')
    {
        return $this->notifier->slack($invoice);
    }

    // ✅ 不用再考慮ELSE 指向哪裡
    return $this->notifier->email($invoice);
}

Note: 有時你會聽到“防衛語句”這樣的術語,它是透過提前返回實現。

#3 - 重構成集合Collection

在 PHP 中,我們在很多不同資料中都用到了陣列。處理及轉換這些陣列可用功能非常有限,並且沒有提供良好的體驗。(array_walk, usort, etc)

要處理這個問題,有一個 Collection 類的概念,可用於幫你處理陣列。最為人所知的是 Laravel 中的實現,其中的 collection 類提供了許多有用的特性,用來處理陣列。

注意: 以下例子, 我將使用 Laravel 的 collect() 輔助函式,不過在其他框架或庫中的使用方式也很相似。

示例 #1

之前

// ❌ 這裡我們有一個臨時變數 
$score = 0;

// ❌ 用迴圈沒有問題,不過可讀性還是有改善空間
foreach($this->playedGames as $game) {
    $score += $game->score;
}

return $score;

之後

// ✅ 集合是帶有方法的物件
// ✅ sum 方法使之更具表現力
return collect($this->playedGames)
    ->sum('score');

示例 #2

之前

$users = [
    [ 'id' => 801, 'name' => 'Peter', 'score' => 505, 'active' => true],
    [ 'id' => 844, 'name' => 'Mary', 'score' => 704, 'active' => true],
    [ 'id' => 542, 'name' => 'Norman', 'score' => 104, 'active' => false],
];

// 請求結果: 只顯示活躍使用者,以 score 排序  ["Mary(704)","Peter(505)"]

$users = array_filter($users, fn ($user) => $user['active']);

// ❌ usort 進行排序處理的又是哪一個物件呢?它是如何實現?
usort($users, fn($a, $b) => $a['score'] < $b['score']);

// ❌ 所有的轉換都是分離的,不過都是users相關的
$userHighScoreTitles = array_map(fn($user) => $user['name'] . '(' . $user['score'] . ')', $users);

return $userHighScoreTitles;

之後

$users = [
    [ 'id' => 801, 'name' => 'Peter', 'score' => 505, 'active' => true],
    [ 'id' => 844, 'name' => 'Mary', 'score' => 704, 'active' => true],
    [ 'id' => 542, 'name' => 'Norman', 'score' => 104, 'active' => false],
];

// 請求結果: 只顯示活躍使用者,以 score 排序  ["Mary(704)","Peter(505)"]

// ✅ 只傳入一次users
return collect($users)
    // ✅ 我們透過管道將其傳入所有方法
  ->filter(fn($user) => $user['active'])
  ->sortBy('score')
  ->map(fn($user) => "{$user['name']} ({$user['score']})"
  ->values()
    // ✅ 最後返回陣列
  ->toArray();

#4 - 一致性

每一行程式碼都會增加少量的視覺噪音。程式碼越多,閱讀起來就越困難。這就是為什麼制定規則很重要。保持類似的東西一致將幫助您識別程式碼和模式。這將導致更少的噪聲和更可讀的程式碼。

示例 #1

之前

class UserController 
{
    // ❌ 確定如何命名變數(駝峰或是蛇形等),不要混用!
    public function find($userId)
    {

    }
}

// ❌ 選擇使用單數或者複數形式命名控制器,並保持一致
class InvoicesController 
{
    // ❌ 修改了樣式,如花扣號的位置,影響可讀性
    public function find($user_id) {

    }
}

之後

class UserController 
{
    // ✅ 所有變數駝峰式命名
    public function find($userId)
    {

    }
}

// ✅ 控制器命名規則一致(此處都使用單數)
class InvoiceController 
{
    // ✅ 花括號的位置(格式)一致,使程式碼更為可讀
    public function find($userId)
    {

    }
}

示例 #2

之前

class PdfExporter
{
    // ❌ "handle" 和 "export" 是類似方法的不同名稱
    public function handle(Collection $items): void
    {
        // export items...
    }
}

class CsvExporter
{
    public function export(Collection $items): void
    {
        // export items...
    }
}

// ❌ 使用時你會疑惑它們是否處理相似的任務
// ❌ 你可能需要再去檢視類原始碼進行確定
$pdfExport->handle();
$csvExporter->export();

之後

// ✅ 可透過介面提供通用規則保持一致性
interface Exporter
{
    public function export(Collection $items): void;
}

class PdfExporter implements Exporter
{
    public function export(Collection $items): void
    {
        // export items...
    }
}

class CsvExporter implements Exporter
{
    public function export(Collection $items): void
    {
        // export items...
    }
}

// ✅ 對類似的任務使用相同的方法名,更具可讀性
// ✅ 不用再去檢視類原始碼,變可知它們都用在匯出資料
$pdfExport->export();
$csvExporter->export();

重構 ❤️ 測試

我已經提到過重構不會改變程式碼的功能。這在執行測試時很方便,因為它們也應該在重構之後工作。這就是為什麼我只有在有測試的時候才開始重構程式碼。他們將確保我不會無意中更改程式碼的行為。所以別忘了寫測試,甚至去TDD。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
Laravel Filament 中文站站長

相關文章