別再用 PHP 動態方法調用了!三個坑讓你代碼難以維護
別再用 PHP 動態方法調用了!三個坑讓你代碼難以維護
你可能在項目代碼里見過這樣的寫法:$this->{'methodName'}() 或者 $this->{$variable}()。這就是動態方法調用,在運行時才確定要調用哪個方法。
看起來很靈活對吧?但用多了你就會發現,這玩意兒會給代碼維護帶來不少麻煩。IDE 找不到引用、全局搜索搜不到、代碼可讀性還差。
本文就來聊聊動態方法調用的三大坑,以及更好的替代方案。
原文鏈接- 別再用 PHP 動態方法調用了!三個坑讓你代碼難以維護
什么是動態方法調用?
正常情況下,我們調用方法都是這樣寫的:$this->methodName()。方法名是寫死的,一眼就能看出來。
但動態方法調用不一樣,它允許你用變量或表達式來決定調用哪個方法:
$methodName = 'doSomething';
$this->{$methodName}(); // 等同于 $this->doSomething()
這種寫法確實靈活,特別是在寫框架或庫的時候,運行時才知道要調用什么方法。但在普通業務代碼里,這種靈活性往往會帶來更多問題。
舉個實際的例子。假設你在做一個 webhook 處理類,根據不同的事件類型調用不同的處理方法。比如收到 success 事件就調用 handleSuccessWebhook,收到 failure 事件就調用 handleFailureWebhook。
webhook 的數據結構大概是這樣:
$payload = [
'event' => 'success',
'details' => [
// ...
],
];
用動態方法調用的話,代碼可能是這樣:
final readonly class WebhookHandler
{
public function handleWebhook(array $payload): void
{
$this->{'handle' . ucfirst($payload['event']) . 'Webhook'}($payload);
}
private function handleSuccessWebhook(array $payload): void
{
// 處理成功事件
}
private function handleFailureWebhook(array $payload): void
{
// 處理失敗事件
}
// 其他處理方法...
}
看到 handleWebhook 方法里那一長串了嗎?它從 $payload['event'] 取值,首字母大寫,拼上 handle 前綴和 Webhook 后綴,最后動態調用這個方法。
看起來挺聰明的,一行代碼搞定所有事件類型。但問題也就藏在這里。
動態方法調用的危險性
現在讓我們探討使用這種方法的一些危險性。
IDE 難以識別
我在動態方法調用中遇到的最大問題之一是集成開發環境(IDE),如 PhpStorm,很難理解它們的使用。由于方法名是在運行時構造的,IDE 很難檢測 handleSuccessWebhook 和 handleFailureWebhook 方法是否真的被使用。這可能導致 IDE 將它們標記為未使用,這可能會產生誤導。
過去,我曾被誘惑刪除 IDE 標記為未使用的方法,后來才發現它們確實通過動態方法調用被使用了。這可能導致應用程序中的錯誤和意外行為。幸運的是,我在部署到生產環境之前及時發現了它們。但那是一次險情。
PhpStorm 無法理解動態方法調用的另一個缺點是你無法充分利用 IDE 的重構工具。例如,如果你想在 PhpStorm 中重命名一個方法,它將無法找到所有引用(因為它們是動態的),也不會為你重命名它們。如果你不小心,這可能導致代碼損壞。
更難查找
我發現動態方法調用的另一個問題是你無法輕松地在代碼庫中搜索它們的使用。
假設你想找到 handleSuccessWebhook 方法被調用的任何地方。所以你在 PhpStorm 中按 CMD+SHIFT+F 打開全局搜索窗口并搜索 "handleSuccessWebhook"。你不會找到任何結果(除了方法定義本身),因為方法名從未在代碼的其他地方明確提及。
此時,你必須問自己:"這個方法在任何地方被使用了嗎?還是可以安全刪除它?"如果你有一個全面的測試套件覆蓋了該特定方法并確認它正在被使用,那么這個問題就會變得更容易回答。但如果你的測試套件沒有覆蓋這個特定功能,那么你必須手動檢查代碼以查看它是否在任何地方被使用。這可能既耗時又容易出錯,尤其是在較大的代碼庫中。
更難閱讀
我個人發現動態方法調用會使代碼在第一眼看上去更難閱讀。你覺得這兩個中哪一個在第一眼看上去更清晰?
// 動態方法調用:
$this->{'handle' . ucfirst($payload['event']) . 'Webhook'}($payload);
// 或者,傳統方法調用:
$this->handleSuccessWebhook($payload);
我猜測大多數人會發現傳統方法調用更清晰。對于動態方法調用,你必須在心里解析字符串連接才能弄清楚正在調用什么方法。如果字符串連接更復雜,或者你不知道 $event 的可能值是什么,這可能特別具有挑戰性。
替代方法
出于上述原因,我通常在代碼中避免使用動態方法調用。相反,我更喜歡使用更明確的方法。
不過,這純粹是我的個人偏好,并不是說動態方法調用本質上是不好的。所以如果你正在閱讀這篇文章并在自己的代碼中使用它們,請不要認為我在侮辱你的代碼。如果它適合你的用例,完全被測試覆蓋,并讓你保持高效,那么我完全支持。
我只是更喜歡使用更明確的方法所帶來的額外信心和安全性。而且我也認為它使代碼更容易閱讀,特別是對于剛接觸代碼庫的開發人員。
如果我要重構上面的 WebhookHandler 類,我可能會使用 match 表達式:
final readonly class WebhookHandler
{
public function handleWebhook(array $payload): void
{
match ($payload['event']) {
'success' => $this->handleSuccessWebhook($payload),
'failure' => $this->handleFailureWebhook($payload),
default => throw new Exception('沒有該事件的處理器: ' . $event)
};
}
private function handleSuccessWebhook(array $payload): void
{
// 在這里處理 "success" webhook...
}
private function handleFailureWebhook(array $payload): void
{
// 在這里處理 "failure" webhook...
}
// 其他 webhook 處理方法...
}
在上面的方法中,我們使用了 "match 表達式"來讀取負載的 event 字段,然后根據其值調用相應的方法。如果事件不被識別,它會拋出一個異常。
通過使用這種方法,我們能夠在代碼中明確寫出方法名。因此,這意味著我們的 IDE 可以理解它們的使用,所以我們可以充分利用其功能(例如重構工具,以及知道將返回哪些類型)。這也意味著我們可以輕松地在代碼庫中搜索它們的使用。我還認為,它使代碼在第一眼看上去更容易閱讀。
結論
希望這篇文章讓你對在 PHP 中使用動態方法調用的危險性有所思考。雖然它們在某些場景中可能很有用,但它們也伴隨著一些你應該注意的風險。

浙公網安備 33010602011771號