簡介
今天在閱讀專案中的老程式碼的時候,發現一些方法的引數特別多,有的甚至超過了十個以上。比如下面這個方法:
...
/**
* 建立訂單
*
* @param int $storeId 店鋪ID
* @param string $storeName 店鋪名稱
* @param int $buyerId 購買者ID
* @param string $buyerName 購買者名稱
* @param int $goodsId 商品ID
* @param string $goodsName 商品名稱
* @param float $price 商品單價
* @param float $amount 商品總價
* @param int $quantity 商品數量
*/
public function createOrder (int $storeId, string $storeName, int $buyerId, string $buyerName, int $goodsId, string $goodsName, float $price, float $amount, int $quantity) {
...
}
...
這是一個建立訂單的方法。透過觀察我們不難發現,在這個方法中,傳遞給方法的引數特別多,這使得程式碼看上去有些「凌亂」,不知道螢幕前的小夥伴有沒有遇到過類似的程式碼呢?
今天這篇文章,我門就來討論下關於「最佳化方法傳參」的問題。
弊端
先不說如何最佳化,我們先來看一下這樣子傳參會有哪些問題。
可讀性
首先一個問題就是「可讀性」的問題。
想象一下,當我們作為方法的「呼叫者」來使用這個方法時我們會怎麼做?
我需要按照固定順序傳遞一堆的引數,每一個引數都必須與定義的型別相匹配。那傳遞這堆引數做什麼用呢?不曉得。
僅從字面意思我們可以大概瞭解一些引數的用途,但是如果我們想了解它的真正含義,我們必須閱讀方法的完整程式碼。
我們知道,方法就像一個「處理器」。簡單來講,主要由輸入引數、處理邏輯和輸出引數組成。而在我們閱讀一個方法時,我們通常會根據方法名、輸入引數和輸出引數來了解方法的基本用途。而當我們面對一堆過於複雜的輸入引數時,我們往往會不知所措。
擴充套件性
可能有過「親身體會」的小夥伴會說:起初的方法其實並沒有這麼複雜,只不過隨著需求的調整,一步步「擴張」成現在這個樣子的。
沒錯,很多方法在設計的時候,可能只有幾個引數,看上去也是簡單易懂。但是當某一天,我們發現我們要實現的功能依賴於外部呼叫方的某個引數時,怎麼辦呢?直接加個引數傳進來唄。
今天加一個,明天加一個,加來加去,當某一天我們回頭來看時,發現我們的引數列表已經變成了一列望不盡頭「小火車」了。
不僅如此,想象一下:當某一天我們不想做「加法」了,想做個「減法」時,會是怎樣的場景呢?
比如,在我們長長的「小火車」中,我們中間的某一節「車廂」不需要了,應該怎麼辦呢?
可能你會說:去掉不就行了?額,如果我告訴你,這個方法有一百個「呼叫方」呢?(打個比方,別當真)
那不好意思了,只能去調整每一個呼叫方的傳參方式了(加油吧,少年)。
約束性
其實我們在說「擴充套件性」問題的時候,做「減法」這個問題也是一個「約束性」問題的體現。
作為「呼叫方」,傳參的順序必須和「定義方」保持一致。所以,當「定義方」引數做出調整時,如果引數是必傳引數,「呼叫方」也需要作出相應調整,正所謂,牽一髮而動全身。
而且,在眾多引數中,每個引數的「重要性」都是不盡相同的。有一些引數需要設計成必傳引數,而有一些引數則可以設計成可選引數,這就要求我們在設計引數順序的時候,必傳的引數往前放,可選的引數往後放。
那現在問題又來了:當我們在已經有了必傳引數和可選引數的情況下,又需要加一個必傳引數該怎麼辦呢?
沒辦法,必傳引數必須放在可選引數前,可這樣一來,整體的引數順序又發生變化了。這就意味著所有的「呼叫方」又該來一遍「大洗牌」了。
可能這就是為什麼我們有時候會看到一個方法設計了一堆引數,但是每個引數都是必傳了。因為雖然不夠合理,但是在增加引數的時候直接往最後邊追加就可以了。
依賴傳遞
還有一種情況,有時我們方法處理的邏輯比較複雜,導致方法體比較「龐大」。所以我們需要將程式碼中的部分邏輯抽離到單獨的方法中去,而抽離出去的方法實現又依賴於原方法的大部分引數,這就會出現以下這種「依賴傳遞」的情況:
...
/**
* 建立訂單
*
* @param int $storeId 店鋪ID
* @param string $storeName 店鋪名稱
* @param int $buyerId 購買者ID
* @param string $buyerName 購買者名稱
* @param int $goodsId 商品ID
* @param string $goodsName 商品名稱
* @param float $price 商品單價
* @param float $amount 商品總價
* @param int $quantity 商品數量
*/
public function createOrder (int $storeId, string $storeName, int $buyerId, string $buyerName, int $goodsId, string $goodsName, float $price, float $amount, int $quantity)
{
// 檢查訂單引數
$this->_checkOrderParameter($storeId, $storeName, $buyerId, $buyerName, $goodsId, $goodsName, $price, $amount, $quantity);
}
/**
* 檢查訂單引數
*
* @param int $storeId 店鋪ID
* @param string $storeName 店鋪名稱
* @param int $buyerId 購買者ID
* @param string $buyerName 購買者名稱
* @param int $goodsId 商品ID
* @param string $goodsName 商品名稱
* @param float $price 商品單價
* @param float $amount 商品總價
* @param int $quantity 商品數量
*/
private function _checkOrderParameter (int $storeId, string $storeName, int $buyerId, string $buyerName, int $goodsId, string $goodsName, float $price, float $amount, int $quantity)
{
...
}
...
如果拆分的引數過多的話,你會發現在一個類很快就變成了一個「火車站」:一排排的「小火車」你連著我,我連著他,相當壯觀。
當然,我也見過有些小夥伴為了解決「依賴傳遞」的問題,將引數都設計成了類變數。這樣看來,方法之間的依賴性是減少了,但是類卻變的更「複雜」了——依賴已經無形中從區域性轉移到全域性。這就好比,當你把兩個人溝通的問題搬到大工作群以後,解決效率是比以前更快了,但可能你同時也失去了一位信任你的朋友。
最佳化
那講了這麼多「問題」,應該怎麼去最佳化呢?
陣列傳參
其實相信很多「經歷」過的小夥伴都嘗試過以下這種方案:
既然主要問題就是引數傳遞的太多,那麼我們減少引數的個數不就行了麼?在保證「資訊量」的基礎上做瘦身不就妥了麼?—— 用更復雜的變數替代簡單型別的變數,比如使用「陣列變數」。
於是,原有的程式碼就被最佳化成了以下這樣:
...
/**
* 建立訂單
*
* @param array $buyerInfo 購買者資訊
* @param array $goodsInfo 商品資訊
* @param array $storeInfo 店鋪資訊
*/
public function createOrder (array $buyerInfo, array $goodsInfo, array $storeInfo)
{
...
}
...
我們可以看到,在上面的最佳化中,我們將引數分別整理到三個陣列中,分別代表購買者資訊、商品資訊和店鋪資訊。
這樣一來,引數個數一下子就從原來的九個變成了現在的三個,看上去簡潔了很多。
但是這樣改造也是存在一些問題的。
首先,用陣列來代替普通型別的變數具有「不確定性」。
比如當我們需要使用購買者 ID($buyerId
)這個欄位時,不能直接使用陣列下標的方式來獲取:
$buyerId = $buyerInfo['buyerId'];
因為 $buyerInfo
變數並不是在上下文中直接定義的,而是透過傳參的方式獲取的。這就意味著作為「接收方」不能完全信任來自「傳遞方」的資料:因為當 $buyerInfo
中沒有傳遞 buyerId
這個索引時,將會丟擲陣列索引未定義的異常。
所以,我們在使用時需要增加索引判斷的邏輯:
$buyerId = $buyerInfo['buyerId'] ?? '';
或者使用 Laravel 的陣列封裝的方法來獲取:
$buyerId = Arr::get($buyerInfo, 'buyerId', '');
如此,雖然解決了使用異常的問題。但是當我們將陣列引數傳遞給其他使用的方法時,原來的方法又成了引數的「傳地方」:這就意味著在新的「接收方」中,又要做一遍引數的驗證處理。
另外,因為 PHP 陣列本身比較靈活,可以隨時定義新的索引,這就意味著傳遞給方法的陣列變數可能比實際要「龐大」很多:因為在實際開發中,可能為了省事,$buyerInfo
這種變數是透過資料庫查詢之後,直接轉換成陣列就丟給了呼叫的方法。雖然陣列包含了方法需要的索引資訊,但同時也冗餘了很多其他資訊。
那有沒有更好一點的傳參方式呢?
物件傳參
其實,除了陣列可以「包含變數」,物件也可以。
而且,物件傳參這種方式並不罕見。在 Laravel 程式碼中隨處可見,比如下面這些場景:
app/Console/Kernel.php
/**
* Define the application's command schedule.
*
* @param Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
...
}
app/Exceptions/Handler.php
/**
* @param Throwable $e
*/
public function report(Throwable $e)
{
...
}
甚至是構造方法的引數:
/**
* 構造方法
*
* @param Request $request
*/
public function __construct(Request $request)
{
...
}
看到這裡,你是不是若有所思:這不就是「依賴注入」嗎?
其實,瞭解「依賴注入」的小夥伴應該都清楚它的核心思想:
依賴注入能夠消除程式開發中的硬編碼式的物件間依賴關係,使應用程式鬆散耦合、可擴充套件和可維護,將依賴性問題的解決從編譯時轉移到執行時。
「依賴注入」主要解決的是模組之間的依賴關係。而在我們今天討論的話題中,側重的是方法之間的呼叫。但是我們也可以借鑑這種處理思想:即透過物件或者介面的方式傳參,從而減少方法對實際使用引數的依賴性。
回到我們正文討論的這種場景上來,我們應該如何設計「物件引數」呢?
其實,物件是具體的實參,我們需要關心的是物件背後「類」的設計,這也是一個「抽象化」設計的過程。
在這個場景中,我們可以將引數進行「抽象化」處理,抽象成請求實體類的屬性。如下:
- 購買者請求類: 購買者 ID、購買者名稱
- 商品請求類: 商品 ID、商品名稱、商品單價、商品總價、商品數量
- 店鋪請求類: 店鋪 ID、店鋪名稱
以 購買者請求類
為例,類程式碼設計如下:
/*
|--------------------------------------------------------------------------
| 購買者請求類
|--------------------------------------------------------------------------
*/
namespace App\Entities\Request;
use http\Exception\InvalidArgumentException;
class BuyerRequest
{
/**
* @var string 購買者ID
*/
private $buyerId;
/**
* @var string 購買者名稱
*/
private $buyerName;
/**
* 獲取購買者名稱
*
* @return string
*/
public function getBuyerId(): string
{
if(is_null($this->buyerId)){
throw new InvalidArgumentException('購買者ID不得為空');
}
return $this->buyerId;
}
/**
* 設定購買者ID
*
* @param string $buyerId 購買者ID
* @return BuyerRequest
*/
public function setBuyerId(string $buyerId): BuyerRequest
{
$this->buyerId = $buyerId;
return $this;
}
/**
* 獲取購買者名稱
*
* @return string
*/
public function getBuyerName(): string
{
return $this->buyerName ?: '';
}
/**
* 設定購買者名稱
*
* @param string $buyerName 購買者名稱
* @return BuyerRequest
*/
public function setBuyerName(string $buyerName): BuyerRequest
{
$this->buyerName = $buyerName;
return $this;
}
}
在類中我們提供了屬性的 getter
和 setter
方法,這也是物件導向中獲取內部屬性的常用做法。
原來的 createOrder
方法調整引數結構如下:
...
/**
* 建立訂單
*
* @param BuyerRequest $buyerRequest 購買者請求實體
* @param GoodsRequest $goodsRequest 商品請求實體
* @param StoreRequest $storeRequest 店鋪請求實體
*/
public function createOrder (BuyerRequest $buyerRequest, GoodsRequest $goodsRequest, StoreRequest $storeRequest)
{
...
}
...
在呼叫方法之前,我們需要先例項化請求物件,然後再傳遞給方法:
...
// 引數例項化
$buyerRequest = (new BuyerRequest())
->setBuyerId('123')
->setBuyerName('測試');
$goodsRequest = new GoodsRequest();
$storeRequest = new StoreRequest();
// 方法呼叫
$this->createOrder($buyerRequest, $goodsRequest, $storeRequest);
...
在 createOrder
方法中,可以透過物件的 getter
方法來獲取具體的引數:
$buyerId = $buyerRequest->getBuyerId();
因為在 getter
方法中包含了引數的處理邏輯,所以可以直接放心使用。
這樣,在開篇提到的那些「弊端」基本上都得到解決了。只不過,我們需要花更多的時間去「抽象化」我們的請求類,但是一旦我們設計好了,無論是對於上游的「呼叫者」還是對於下游的「使用者」來說,都是十分便捷的。
總結
本篇文章我們討論了關於程式開發中傳參的設計。透過分析幾種不同的傳參方式,我們大致瞭解了各種方式的利弊。雖然「物件傳參」的方式看上去更優雅,但是也要考慮實際情況的需要,避免陷入「過度設計」的怪圈。
所有的設計最終都要從實際生產環境出發。優雅值得關注,效能更不能忽視。
感謝大家的持續關注~
本作品採用《CC 協議》,轉載必須註明作者和本文連結