你是如何處理 PHP 代碼中的枚舉類型 Enum 的?

文章轉發自專業的Laravel開發者社區,原始鏈接:https://learnku.com/laravel/t...

本文旨在提供一些更好的理解什麼是枚舉,什麼時候使用它們以及如何在php中使用它們.

我們在某些時候使用了常量來定義代碼中的一些常數值.他們被用來避免魔法值.用一個象徵性的名字代替一些魔法值,我們可以給它一些意義.然後我們在代碼中引用這個符號名稱.因爲我們定義了一次並使用了很多次,所以搜索它並稍後重命名或更改一個值會更容易.

這就是爲什麼看到類似於下面的代碼並不罕見.


<?php
class User {
    const GENDER_MALE = 0;
    const GENDER_FEMALE = 1;
    const STATUS_INACTIVE = 0;
    const STATUS_ACTIVE = 1;
}

以上常量表示了兩組屬性,GEDNER_* 和 STATUS_*。他們表示一組性別和一組用戶狀態。每一組都是一個枚舉 。枚舉是一組元素(也叫做成員)的集合,每一個枚舉都定義了一種新類型。這個類型,和它的值一樣,可以包含任意屬於該枚舉的元素。

在上面的例子中,枚舉藉助於常量,每一個常量的值都是一個成員。注意,這樣做的話,我們只能在常量包含的類型中取值。因此,我們在寫這些值的時候不會有類型提示,不知道詳細的枚舉類型。

來看一個簡短的例子, 但我們假定例子中有更多的代碼

<?php
interface UserFactory {
    public function create(
        string $email,
        int $gender,
        int $status
    ): User;
}
$factory->create(
    $email,
    User::STATUS_ACTIVE,
    User::GENDER_FEMALE
);

第一眼看上去代碼很好,但是他只是碰巧正確運行了!因爲兩個不同的枚舉成員實際上是同一個值,調用create方法成功,是因爲這最後兩個參數被互換了不影響結果。儘管我們檢查方法接受的值是否有效,運行界面也不會警告我們,測試也會通過。有人能正確的發現這些bug,但是它也很可能被忽視掉。之後一些情況,比如合併衝突的時候,如果它的值改變了,它可能會引起系統異常。

如果使用標量類型,我們會受限於這種類型,無法辨別這兩個值是是不是屬於兩個不同的枚舉。

另一個問題是這個代碼描述的的不是很好。想象一下 create 方法沒有引用常量。$gender 被別人看作爲一個枚舉元素將是有多麼困難?看這些元素在哪裏被定義又有多麼困難?我們之後將會閱讀那些代碼,因此我們應該儘可能是讓代碼易於閱讀以及和通過。

我們可以做得更好嗎? Sure! 這個方法就是是使用類實例作爲枚舉元素,類本身定義了一個新的類型。 直到PHP 7,我們可以安裝 SPL類 PECL擴展並且使用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

我們擴展 SplEnum 並且定義用於創建枚舉元素的常量。枚舉元素是我們手動構造的對象,在這種情況下是常量值本身。 我們可以將整型與對象進行比較,這可能很奇怪。 另外,正如文檔所述,這是一個仿真的枚舉。 PHP本身並不支持枚舉類型,所以我們在這裏探討的所有內容都是仿真的。

我們用這種方法得到了什麼? 我們可以輸入提示我們的參數,並讓PHP引擎在發生錯誤時提醒我們。 我們還可以在枚舉類中包含一些邏輯,並使用switch語句來模擬多態行爲。

但也有一些缺點. 例如, 在大多數情況下, 有些你可以用枚舉元素而不能用標識檢查. 這不是不可能的,我們不得不非常小心. 由於我們手動創建枚舉成員, 所以許多成員應該是同一個成員, 但這一點手動很難確定.

利用 SplEnum 我們解決枚舉類型問題, 但是當我們用標識檢查的時候不得不非常小心. 我們需要一個方法限制可以創建的多個元素, 例如  multiton (multiple singleton objects).

現在我們將看到由 Java Enum 啓發並實現 multiton 的兩個不同的庫.

第一個是 eloquent/enumeration. 它爲每個元素創建一個定義類的實例. 請注意, 沒有我們的幫助, 枚舉的用戶仿真永遠不能保證一個枚舉實例, 因爲我們限制它的每一步都有一個方法去避免.

這個庫可以讓我們用錯誤的方式去嘗試, 例如用反射創建一個實例, 在這一點上我們可以問我們自己是否做了正確的事. 它也可以在代碼的評審過程中有所幫助,因爲這樣的實現可以定義幾個應該被遵循的規則. 如果這些規則比較簡單很容易發現代碼中存在的問題.

讓我們看些實例.


<?php
final class YesNo extends \Eloquent\Enumeration\AbstractEnumeration {
    const NO = 0;
    const YES = 1;
}
var_dump(YesNo::YES()->key()); // YES

我們定義了一個繼承  \Eloquent\Enumeration\AbstractEnumeration 的新類 YesNo . 接下來我們定義一個定義元素名和創建表現這些元素的對象的庫的常量.

還有一些情況我們需要謹記,用 serialize/deserialize 在其中創建自定義對象 .

我們可以在GitHub頁面上找到更多的例子和很完善的文檔。

我們要展示的第二個庫是 zlikavac32/php-enum. 與 eloquent/enumeration不同,這個庫面向允許真正的多態行爲的抽象類。 所以,我們可以用每個方法都定義一個枚舉元素來實現,而不是使用switch的方法。 通過嚴格的規則來定義枚舉,也可以相當可靠地確保每個元素只有一個實例。

這個庫面向抽象類,以便將每個成員的許多實例限制爲一個。 這個想法是,每個枚舉必須被定義爲抽象的,並枚舉它的元素。 請注意,你可以通過擴展類,然後構造一個元素來濫用,但是如果你這麼用了,這些是會在代碼審查過程中標紅的。

對於抽象類,我們知道我們不會意外地有一個枚舉的新元素,因爲它需要具體的實現。 通過遵循在enum本身中保持這些具體實現的規則,我們可以很容易地發現濫用。  匿名類 在這裏很有用。

庫強制抽象枚舉類,但不能強制創建有效的元素。 這是這個庫的用戶的責任。 圖書館照顧其餘的。

讓我們看一個簡單的例子。


<?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註釋定義了返回枚舉元素的現有靜態方法。 這有助於搜索和重構代碼。 接下來,我們將枚舉YesNo定義爲抽象,並擴展\Zlikavac32\Enum\Enum並定義一個靜態方法enumerate。 然後,在enumerate方法中,我們列出將被用來表示它們的元素名稱。

剛剛我們提到了多態行爲,那麼爲什麼我們會使用它呢? 當我們試圖限制同一個枚舉元素的多個實例時會發生一件事,那就是我們不能有循環引用。 讓我們想象一下,我們想擁有由NORTHSOUTHEASTWEST組成的WorldSide枚舉。 我們還想有一個方法opposite():WorldSide,它返回代表相反的元素。
如果我們試圖通過構造函數注入相反元素,在某一時刻,我們獲得一個循環引用,這意味着,我們需要相同元素的第二個實例。 爲了返回一個有效的相反世界,我們不得不用一個代理對象 或者switch語句破解。

隨着多態行爲,我們能做的就是讓我們看到我們可定義我們需要的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 方法,我們提供了每一個枚舉元素的實現。數組是用枚舉元素名稱來索引的。當手動的創建元素,我們定義我們元素名稱作爲數據的鍵。

我們可以用 WorldSide::iterator() 獲取枚舉元素的順序迭代器,來定義和遍歷他們。 每一個枚舉元素都有一個默認的 __toString(): string實現返回元素的名稱。

每個枚舉元素返回其相反的元素。

回顧一下,常量不是枚舉,枚舉不是常量。每個枚舉定義一個類型。如果我們有一些常數的值對我們很重要,但名字沒有,我們應該堅持常數。如果我們有一些常量的價值對我們無關緊要,但是與同一羣體中的其他所有人有所不同則是重要的,請使用枚舉

枚舉爲代碼提供了更多的上下文,也可以將某些檢查委託給引擎本身。如果PHP有一個本地的枚舉支持,這將是非常好的。語法更改可以使代碼更具可讀性。引擎可以爲我們執行檢查,並執行一些不能從用戶區執行的規則。

你如何使用枚舉,你對這個主題有什麼想法?請在下方評論

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章