騰云網(wǎng)絡(luò)教你如何處理 PHP 代碼中的枚舉類型 Enum 的
2019-10-12
本文旨在提供一些更好的理解什么是枚舉,什么時(shí)候使用它們以及如何在php中使用它們.
我們?cè)谀承r(shí)候使用了常量來(lái)定義代碼中的一些常數(shù)值.他們被用來(lái)避免 魔法值 .用一個(gè)象征性的名字代替一些 魔法值 ,我們可以給它一些意義.然后我們?cè)诖a中引用這個(gè)符號(hào)名稱.因?yàn)槲覀兌x了一次并使用了很多次,所以搜索它并稍后重命名或更改一個(gè)值會(huì)更容易.
這就是為什么看到類似于下面的代碼并不罕見(jiàn).
<?php class User { const GENDER_MALE = 0; const GENDER_FEMALE = 1; const STATUS_INACTIVE = 0; const STATUS_ACTIVE = 1; }
以上常量表示了兩組屬性,GEDNER_* 和 STATUS_*。他們表示一組性別和一組用戶狀態(tài)。每一組都是一個(gè) 枚舉 。枚舉是一組元素(也叫做成員)的集合,每一個(gè)枚舉都定義了一種新類型。這個(gè)類型,和它的值一樣,可以包含任意屬于該枚舉的元素。
在上面的例子中,枚舉借助于常量,每一個(gè)常量的值都是一個(gè)成員。注意,這樣做的話,我們只能在常量包含的類型中取值。因此,我們?cè)趯?xiě)這些值的時(shí)候不會(huì)有類型提示,不知道詳細(xì)的枚舉類型。
來(lái)看一個(gè)簡(jiǎn)短的例子, 但我們假定例子中有更多的代碼
<?php interface UserFactory { public function create( string $email, int $gender, int $status ): User; } $factory->create( $email, User::STATUS_ACTIVE, User::GENDER_FEMALE );
第一眼看上去代碼很好,但是他只是碰巧正確運(yùn)行了!因?yàn)閮蓚€(gè)不同的枚舉成員實(shí)際上是同一個(gè)值,調(diào)用create方法成功,是因?yàn)檫@最后兩個(gè)參數(shù)被互換了不影響結(jié)果。盡管我們檢查方法接受的值是否有效,運(yùn)行界面也不會(huì)警告我們,測(cè)試也會(huì)通過(guò)。有人能正確的發(fā)現(xiàn)這些bug,但是它也很可能被忽視掉。之后一些情況,比如合并沖突的時(shí)候,如果它的值改變了,它可能會(huì)引起系統(tǒng)異常。
如果使用標(biāo)量類型,我們會(huì)受限于這種類型,無(wú)法辨別這兩個(gè)值是是不是屬于兩個(gè)不同的枚舉。
另一個(gè)問(wèn)題是這個(gè)代碼描述的的不是很好。想象一下 create
方法沒(méi)有引用常量。 $gender
被別人看作為一個(gè)枚舉元素將是有多么困難?看這些元素在哪里被定義又有多么困難?我們之后將會(huì)閱讀那些代碼,因此我們應(yīng)該盡可能是讓代碼易于閱讀以及和通過(guò)。
我們可以做得更好嗎?Sure!這個(gè)方法就是是使用類實(shí)例作為枚舉元素,類本身定義了一個(gè)新的類型。直到PHP 7,我們可以安裝 SPL類 PECL擴(kuò)展并且使用 SplEnum 。
<?php class YesNo extends \SplEnum { const __default = self::YES; const NO = 0; const YES = 1; } $no = new YesNo(YesNo::NO); var_dump($no == YesNo::NO); //true var_dump(new YesNo(YesNo::NO) == YesNo::NO); //true
我們擴(kuò)展 SplEnum
并且定義用于創(chuàng)建枚舉元素的常量。枚舉元素是我們手動(dòng)構(gòu)造的對(duì)象,在這種情況下是常量值本身。我們可以將整型與對(duì)象進(jìn)行比較,這可能很奇怪。另外,正如文檔所述,這是一個(gè)仿真的枚舉。PHP本身并不支持枚舉類型,所以我們?cè)谶@里探討的所有內(nèi)容都是仿真的。
我們用這種方法得到了什么?我們可以輸入提示我們的參數(shù),并讓PHP引擎在發(fā)生錯(cuò)誤時(shí)提醒我們。我們還可以在枚舉類中包含一些邏輯,并使用 switch
語(yǔ)句來(lái)模擬多態(tài)行為。
但也有一些缺點(diǎn). 例如, 在大多數(shù)情況下, 有些你可以用枚舉元素而不能用標(biāo)識(shí)檢查. 這不是不可能的,我們不得不非常小心. 由于我們手動(dòng)創(chuàng)建枚舉成員, 所以許多成員應(yīng)該是同一個(gè)成員, 但這一點(diǎn)手動(dòng)很難確定.
利用 SplEnum
我們解決枚舉類型問(wèn)題, 但是當(dāng)我們用標(biāo)識(shí)檢查的時(shí)候不得不非常小心. 我們需要一個(gè)方法限制可以創(chuàng)建的多個(gè)元素, 例如 multiton (multiple singleton objects ).
現(xiàn)在我們將看到由 Java Enum 啟發(fā)并實(shí)現(xiàn) multiton
的兩個(gè)不同的庫(kù).
第一個(gè)是 eloquent/enumeration . 它為每個(gè)元素創(chuàng)建一個(gè)定義類的實(shí)例. 請(qǐng)注意, 沒(méi)有我們的幫助, 枚舉的用戶仿真永遠(yuǎn)不能保證一個(gè)枚舉實(shí)例, 因?yàn)槲覀兿拗扑拿恳徊蕉加幸粋€(gè)方法去避免.
這個(gè)庫(kù)可以讓我們用錯(cuò)誤的方式去嘗試, 例如用反射創(chuàng)建一個(gè)實(shí)例, 在這一點(diǎn)上我們可以問(wèn)我們自己是否做了正確的事. 它也可以在代碼的評(píng)審過(guò)程中有所幫助,因?yàn)檫@樣的實(shí)現(xiàn)可以定義幾個(gè)應(yīng)該被遵循的規(guī)則. 如果這些規(guī)則比較簡(jiǎn)單很容易發(fā)現(xiàn)代碼中存在的問(wèn)題.
讓我們看些實(shí)例.
<?php final class YesNo extends \Eloquent\Enumeration\AbstractEnumeration { const NO = 0; const YES = 1; } var_dump(YesNo::YES()->key()); // YES
我們定義了一個(gè)繼承 \Eloquent\Enumeration\AbstractEnumeration
的新類 YesNo
. 接下來(lái)我們定義一個(gè)定義元素名和創(chuàng)建表現(xiàn)這些元素的對(duì)象的庫(kù)的常量.
還有一些情況我們需要謹(jǐn)記,用 serialize
/ deserialize
在其中創(chuàng)建自定義對(duì)象 .
我們可以在GitHub頁(yè)面上找到更多的例子和很完善的文檔。
我們要展示的第二個(gè)庫(kù)是 zlikavac32/php-enum . 與 eloquent/enumeration
不同,這個(gè)庫(kù)面向允許真正的多態(tài)行為的抽象類。所以,我們可以用每個(gè)方法都定義一個(gè)枚舉元素來(lái)實(shí)現(xiàn),而不是使用 switch
的方法。通過(guò)嚴(yán)格的規(guī)則來(lái)定義枚舉,也可以相當(dāng)可靠地確保每個(gè)元素只有一個(gè)實(shí)例。
這個(gè)庫(kù)面向抽象類,以便將每個(gè)成員的許多實(shí)例限制為一個(gè)。這個(gè)想法是,每個(gè)枚舉必須被定義為抽象的,并枚舉它的元素。請(qǐng)注意,你可以通過(guò)擴(kuò)展類,然后構(gòu)造一個(gè)元素來(lái)濫用,但是如果你這么用了,這些是會(huì)在代碼審查過(guò)程中標(biāo)紅的。
對(duì)于抽象類,我們知道我們不會(huì)意外地有一個(gè)枚舉的新元素,因?yàn)樗枰唧w的實(shí)現(xiàn)。通過(guò)遵循在enum本身中保持這些具體實(shí)現(xiàn)的規(guī)則,我們可以很容易地發(fā)現(xiàn)濫用。 匿名類 在這里很有用。
庫(kù)強(qiáng)制抽象枚舉類,但不能強(qiáng)制創(chuàng)建有效的元素。這是這個(gè)庫(kù)的用戶的責(zé)任。圖書(shū)館照顧其余的。
讓我們看一個(gè)簡(jiǎn)單的例子。
<?php /** * @method static YesNo YES * @method static YesNo NO */ abstract class YesNo extends \Zlikavac32\Enum\Enum { protected static function enumerate(): array { return [ 'YES', 'NO' ]; } } var_dump(YesNo::YES()->name()); // YES
PHPDoc注釋定義了返回枚舉元素的現(xiàn)有靜態(tài)方法。這有助于搜索和重構(gòu)代碼。接下來(lái),我們將枚舉 YesNo
定義為抽象,并擴(kuò)展 \Zlikavac32\Enum\Enum
并定義一個(gè)靜態(tài)方法 enumerate
。然后,在 enumerate
方法中,我們列出將被用來(lái)表示它們的元素名稱。
剛剛我們提到了多態(tài)行為,那么為什么我們會(huì)使用它呢?當(dāng)我們?cè)噲D限制同一個(gè)枚舉元素的多個(gè)實(shí)例時(shí)會(huì)發(fā)生一件事,那就是我們不能有循環(huán)引用。讓我們想象一下,我們想擁有由 NORTH
, SOUTH
, EAST
和 WEST
組成的 WorldSide
枚舉。我們還想有一個(gè)方法 opposite():WorldSide
,它返回代表相反的元素。
如果我們?cè)噲D通過(guò)構(gòu)造函數(shù)注入相反元素,在某一時(shí)刻,我們獲得一個(gè)循環(huán)引用,這意味著,我們需要相同元素的第二個(gè)實(shí)例。為了返回一個(gè)有效的相反世界,我們不得不用一個(gè) 代理對(duì)象 或者 switch
語(yǔ)句破解。
隨著多態(tài)行為,我們能做的就是讓我們看到我們可定義我們需要的 WorldSide
枚舉。
<?php /** * @method static WorldSide NORTH * @method static WorldSide SOUTH * @method static WorldSide EAST * @method static WorldSide WEST */ abstract class WorldSide extends \Zlikavac32\Enum\Enum { protected static function enumerate(): array { return [ 'NORTH' => new class extends WorldSide { public function opposite(): WorldSide { return WorldSide::SOUTH(); } }, 'SOUTH' => new class extends WorldSide { public function opposite(): WorldSide { return WorldSide::NORTH(); } }, 'EAST' => new class extends WorldSide { public function opposite(): WorldSide { return WorldSide::WEST(); } }, 'WEST' => new class extends WorldSide { public function opposite(): WorldSide { return WorldSide::EAST(); } } ]; } abstract public function opposite(): WorldSide; } foreach (WorldSide::iterator() as $worldSide) { var_dump(sprintf( 'Opposite of %s is %s', (string) $worldSide, (string) $worldSide->opposite() )); }
在 enumerate
方法,我們提供了每一個(gè)枚舉元素的實(shí)現(xiàn)。數(shù)組是用枚舉元素名稱來(lái)索引的。當(dāng)手動(dòng)的創(chuàng)建元素,我們定義我們?cè)孛Q作為數(shù)據(jù)的鍵。
我們可以用 WorldSide::iterator()
獲取枚舉元素的順序迭代器,來(lái)定義和遍歷他們。每一個(gè)枚舉元素都有一個(gè)默認(rèn)的 __toString(): string
實(shí)現(xiàn)返回元素的名稱。
每個(gè)枚舉元素返回其相反的元素。
回顧一下,常量不是枚舉,枚舉不是常量。每個(gè)枚舉定義一個(gè)類型。如果我們有一些常數(shù)的值對(duì)我們很重要,但名字沒(méi)有,我們應(yīng)該堅(jiān)持常數(shù)。如果我們有一些常量的價(jià)值對(duì)我們無(wú)關(guān)緊要,但是與同一群體中的其他所有人有所不同則是重要的,請(qǐng)使用枚舉
枚舉為代碼提供了更多的上下文,也可以將某些檢查委托給引擎本身。如果PHP有一個(gè)本地的枚舉支持,這將是非常好的。語(yǔ)法更改可以使代碼更具可讀性。引擎可以為我們執(zhí)行檢查,并執(zhí)行一些不能從用戶區(qū)執(zhí)行的規(guī)則。