PHP 自定義集合與陣列規範
下面是我使用處理動態陣列的一些規則。這差不多是一個關於陣列設計的風格指南,但是把它新增到物件設計風格指南感覺不太對,因為不是所有的面嚮物件語言都有動態陣列。本文中的示例是用 PHP 編寫的,因為 PHP 很像 Java (可能比較熟悉),但是使用的是動態陣列而不是內建的集合類和介面。
使用陣列作為列表
所有元素都應該具有相同的型別
當使用一個數組作為一個列表 (一個具有特定順序的值的集合) 時,每個值應該是 z 型別:
$goodList = [ 'a', 'b' ]; $badList = [ 'a', 1 ];
一個被普遍接受的註釋列表型別的風格是:@var array<TypeOfElement>
int
)。
應該忽略每個元素的索引
PHP 將自動為列表中的每個元素 (0、1、2 等) 建立新索引。然而,你不應該依賴這些索引,也不應該直接使用它們。客戶端應該依賴的列表的唯一屬性是可迭代的和可計數的。
因此,可以隨意使用foreach
和count()
,但不要使用for
迴圈遍歷列表中的元素:
// 好的迴圈: foreach ($list as $element) { } // 不好的迴圈 (公開每個元素的索引): foreach ($list as $index => $element) { } // 也是不好的迴圈 (不應該使用每個元素的索引): for ($i = 0; $i < count($list); $i++) { }
(在 PHP 中,for
迴圈甚至可能不起作用,因為列表中可能缺少索引,而且索引可能比列表中的元素數量還要多。)
使用過濾器而不是刪除元素
你可能希望通過索引從列表中刪除元素 (unset()
),但是,你應該使用array_filter()
來建立一個新列表 (沒有不需要的元素),而不是刪除元素。
同樣,你不應該依賴於元素的索引,因此,在使用array_filter()
時,不應該使用ref="https://www.php.net/manual/en/function.array-filter.php">flag 引數去根據索引過濾元素,甚至根據元素和索引過濾元素。
// 好的過濾: array_filter( $list, function (string $element): bool { return strlen($element) > 2; } ); // 不好的過濾器(也使用索引來過濾元素) array_filter( $list, function (int $index): bool { return $index > 3; }, ARRAY_FILTER_USE_KEY ); // 不好的過濾器(同時使用索引和元素來過濾元素) array_filter( $list, function (string $element, int $index): bool { return $index > 3 || $element === 'Include'; }, ARRAY_FILTER_USE_BOTH );
使用陣列作為對映
當鍵是相關的,而不是索引 (0,1,2,等等)。你可以隨意使用陣列作為對映 (可以通過其唯一的鍵從其中檢索值)。
所有的鍵應該是相同的型別
使用陣列作為對映的第一個規則是,陣列中的所有鍵都應該具有相同的型別 (最常見的是string
型別的鍵)。
$goodMap = [ 'foo' => 'bar', 'bar' => 'baz' ]; // 不好(使用不同型別的鍵) $badMap = [ 'foo' => 'bar', 1 => 'baz' ];
所有的值都應該是相同的型別
對映中的值也是如此:它們應該具有相同的型別。
$goodMap = [ 'foo' => 'bar', 'bar' => 'baz' ]; // 不好(使用不同型別的值) $badMap = [ 'foo' => 'bar', 'bar' => 1 ];
一種普遍接受的對映型別註釋樣式是:@var array<TypeOfKey, TypeOfValue>
。
對映應該保持私有
列表可以安全地在物件之間傳遞,因為它們具有簡單的特徵。任何客戶端都可以使用它來迴圈其元素,或計數其元素,即使列表是空的。對映則更難處理,因為客戶端可能依賴於沒有對應值的鍵。這意味著在一般情況下,它們應該對管理它們的物件保持私有。不允許客戶端直接訪問內部對映,而是提供 getter (可能還有 setter) 來檢索值。如果請求的鍵不存在值,則丟擲異常。但是,如果您可以保持對映及其值完全私有,那麼就這樣做。
// 公開一個列表是可以的 /** * @return array<User> */ public function allUsers(): array { // ... } // 公開地圖可能很麻煩 /** * @return array<string, User> */ public function usersById(): array { // ... } // 相反,提供一種方法來根據其鍵檢索值 /** * @throws UserNotFound */ public function userById(string $id): User { // ... }
對具有多個值型別的對映使用物件
當你想要在一個對映中儲存不同型別的值時,請使用一個物件。定義一個類,並向其新增公共的型別化屬性,或新增建構函式和 getter。像這樣的物件的例子是配置物件,或者命令物件:
final class SillyRegisterUserCommand { public string $username; public string $plainTextPassword; public bool $wantsToReceiveSpam; public int $answerToIAmNotARobotQuestion; }
這些規則的例外
有時,庫或框架需要以更動態的方式使用陣列。在這些情況下,不可能 (也不希望) 遵循前面的規則。例如陣列資料,它將被儲存在一個數據庫表中,或者 Symfony表單配置。
自定義集合類
自定義集合類是一種非常酷的方法,最後可以和Iterator
、ArrayAccess
和其朋友一起使用,但是我發現大多數生成的程式碼令人很困惑。第一次檢視程式碼的人必須在 PHP 手冊中查詢詳細資訊,即使他們是有經驗的開發人員。另外,你需要編寫更多的程式碼,你必須維護這些程式碼 (測試、除錯等)。所以在大多數情況下,我發現一個簡單的陣列,加上一些適當的型別註釋,就足夠了。到底什麼是需要將陣列封裝到自定義集合物件中的強訊號?
- 如果你發現與那個陣列相關的邏輯被複制了。
- 如果你發現客戶端必須處理太多關於陣列內部內容的細節。
使用自定義集合類來防止重複邏輯
如果使用相同陣列的多個客戶端執行相同的任務 (例如過濾、對映、減少、計數),則可以通過引入自定義集合類來消除重複。將重複的邏輯移到集合類的一個方法上,允許任何客戶端使用對集合的簡單方法呼叫來執行相同的任務:
$names = [/* ... */]; // 在幾個地方發現: $shortNames = array_filter( $names, function (string $element): bool { return strlen($element) < 5; } ); // 變成一個自定義集合類: use Assert\Assert; final class Names { /** * @var array<string> */ private array $names; public function __construct(array $names) { Assert::that()->allIsString($names); $this->names = $names; } public function shortNames(): self { return new self( array_filter( $this->names, function (string $element): bool { return strlen($element) < 5; } ) ); } } $names = new Names([/* ... */]); $shortNames = $names->shortNames();
在集合的轉換上使用方法的好處就是獲得了一個名稱。這使你能夠向看起來相當複雜的array_filter()
呼叫新增一個簡短而有意義的標籤。
使用自定義集合類來解耦客戶端
如果一個客戶端使用特定的陣列並迴圈,從選定的元素中取出一段資料,並對該資料進行處理,那麼該客戶端就與所有涉及的型別緊密耦合:陣列本身、陣列中元素的型別、它從所選元素中檢索的值的型別、選擇器方法的型別,等等。這種深度耦合的問題是,在不破壞依賴於它們的客戶端的情況下,很難更改所涉及型別的任何內容。因此,在這種情況下,你也可以將陣列包裝在一個自定義 的集合類中,讓它一次性給出正確的答案,在內部進行必要的計算,讓客戶端與集合更加鬆散地耦合。
$lines = []; $sum = 0; foreach ($lines as $line) { if ($line->isComment()) { continue; } $sum += $line->quantity(); } // Turned into a custom collection class: final class Lines { public function totalQuantity(): int { $sum = 0; foreach ($lines as $line) { if ($line->isComment()) { continue; } $sum += $line->quantity(); } return $sum; } }
自定義集合類的一些規則
讓我們看看在使用自定義集合類時應用的一些規則。
讓它們不可變
對集合例項的現有引用在執行某種轉換時不應受到影響。因此,任何執行轉換的方法都應該返回類的一個新例項,就像我們在上面的例子中看到的那樣:
final class Names { /** * @var array<string> */ private array $names; public function __construct(array $names) { Assert::that()->allIsString($names); $this->names = $names; } public function shortNames(): self { return new self( /* ... */ ); } }
當然,如果要對映內部陣列,則可能要對映到另一種型別的集合或簡單陣列。與往常一樣,請確保提供適當的返回型別。
只提供實際客戶需要和使用的行為
你不必擴充套件泛型集合庫類,也不必自己在每個自定義集合類上實現泛型篩選器、對映和縮減方法,只實現真正需要的。如果某個方法在某一時刻不被使用,那麼就刪除它。
使用 IteratorAggregate 和 ArrayIterator 來支援迭代
如果你使用 PHP,不用實現所有的Iterator
介面的方法 (並保持一個內部指標,等等),只是實現IteratorAggregate
介面,讓它返回一個ArrayIterator
例項基於內部陣列:
final class Names implements IteratorAggregate { /** * @var array<string> */ private array $names; public function __construct(array $names) { Assert::that()->allIsString($names); $this->names = $names; } public function getIterator(): Iterator { return new ArrayIterator($this->names); } } $names = new Names([/* ... */]); foreach ($names as $name) { // ... }
權衡考慮
為你的自定義集合類編寫更多程式碼的好處是使客戶端更容易地使用該集合 (而不是僅僅使用一個數組)。如果客戶端程式碼變得更容易理解,如果集合提供了有用的行為,那麼維護自定義集合類的額外成本就是合理的。但是,因為使用動態陣列非常容易 (主要是因為你不必寫出所涉及的型別),所以我不經常介紹自己的集合類。儘管如此,我知道有些人是它們的偉大支持者,所以我將確保繼續尋找潛在的用例。