在程式設計中,列舉(enum)是個常見需求,現在不少語言也都有列舉相關的元素,常見的應用模式之一,是列舉一組名稱,然後在判斷式中比對,並做出相對應的動作,然而,在大部份的情況下,這樣的設計很可能違反了開放封閉原則。
型態安全列舉
如果有個變數WEEK,需要設定星期一到星期日的值,例如MON、TUE到SUN,在沒有專用列舉型態的語言中,必須賦值給MON、TUE等變數,然後在必要時,將其中一個變數指定給WEEK,進一步地,既然MON、TUE到SUN都是WEEK可接受的值,不如使用WEEK_MON、WEEK_TUE、WEEK_SUN,以WEEL_前置名稱來呈現這組值的相關性。
這就是列舉需求的出發點,有些語言中,可以會使用WEEK.MON、WEEK.TUE這樣的方式,讓WEEK作為名稱空間來呈現列舉值相關性。
在沒有專用列舉型態的語言中,通常會選擇使用整數或字串來作為列舉的值,這稱為整數列舉模式或字串列舉模式。
然而,問題顯而易見的部分在於,開發者對值的指定通常是隨意的,不見得是有序的,由於本質上列舉值仍是整數或字串,因此,API上仍然可以傳入任意的整數或字串,這會引發一些不可預期的問題,在設計列舉的比對時,若少輸入或多輸入了不必要的列舉值,編譯或程式執行時,都不會有任何的提示。
對於靜態定型語言,現在不少都實現了型態安全列舉(Typesafe Enum)。
以Java為例,從JDK5開始,可使用enum關鍵字來定義列舉,每個被列舉的元素都有個專屬型態,而且實現了單例(Singleton)模式,編譯器可以檢查出列舉值的型態是否符合,在列舉比對時,也可以檢查出列舉成員是否完備,以及是否有非成員之一的列舉。
在動態定型語言的部分,也可以從型態安全列舉的概念中獲益。
例如,在Python 3.4之前,有著各種模擬(型態安全)列舉的方式,而Python 3.4實現了PEP 435,可以繼承enum.Enum來實現列舉,除了避免魔法數字的相關問題之外,也統一了過去各種模稜兩可的列舉實現或者錯誤訊息,希望對列舉的實作及錯誤訊息的呈現,能起到標準化的作用。
列舉作為組態之用?
由於列舉通常會用來定義一組有限元素的集合,因此常被拿來作為組態時可用的選項列舉,API客戶端可以提供不同的選項,常見的模式,就是使用switch或if...else之類的元素來進行比對,以便程式庫就會作出相對應的處理。
有些自然存在的有限元素集合,確實天生適合進行列舉,像是星期、月份、年、月、日這類的時間單位,一個星期就是有星期一到星期日,這是固定不會變的成員;有時,列舉可能是一組相對不易變動的有限選項,像是對應至作業系統底層的一些選項,例如檔案系統的讀寫權限設定。
不過,像是使用者介面設定之類的選項,可能就不適合使用列舉,因為介面的選項在未來變動的可能性很高。
在〈Enums as configuration: the anti-pattern〉這篇文章中,就有個範例,在指定不同的列舉下,給予使用者介面不同的設定,未來要增加使用者介面的選項時,除了要修改列舉值,也要修改程式庫中相對應的程式碼,顯然是個沒有彈性的設計方式。
問題正在於,雖然程式庫想要提供使用者設定介面上的「彈性」,然而,列舉本身實際上並「不」具備彈性,因為它本來就是用來定義一組有限數量的元素,如果選項未來變動可能性大,使用組態物件(configuration object)反而會是比較可行的方式,如此一來,日後不用為了增加選項而修改列舉,也不用修改對應的switch之類的清單,符合SOLID中所提到的開放封閉原則。
列舉與行為擴充性
列舉本身是用來定義一組有限的集合,實際上,並不具備彈性也是個事實,因此,實現型態安全列舉的語言中,列舉多半不具擴充性,也就是不能繼承一個列舉來新增成員。儘管Python 3.4允許列舉的繼承,然而,限制條件是,父列舉中不能有任何的列舉元素,只能定義一些公用方法。這樣的父列舉與其說是列舉,不如說像是個Mixin類別。
在《Effective Java》第二版第34條目中,也談到了這點:「列舉的可擴充性,最後都證明不是什麼好主意。」
如果每個列舉成員,實際上都對應於某個行為,除了使用switch之類的元素來比對並執行對應動作之外,在Java中,支援為特定成員值實現不同的類別本體(Value-Specific Class Bodies),如果使用介面來定義行為,並結合物件導向的次型態多型,就可以避免使用switch之類的元素,例如《Effective Java》中的例子:
public enum BasicOperation implements Operation { PLUS("+") { public double apply(double x, double y) { return x + y; } } ...
進一步地,如果開發者開始思考,想要擴充列舉成員,並讓被擴充的成員能夠有對應的擴充行為,那麼可循著「擴充行為」的方向來進行,而不是擴充列舉成員:
public enum ExtendedOperation implements Operation { EXP("^") { public double apply(double x, double y) { return Math.pow(x, y); } } ...
實際上,BasicOperation與ExtendedOperation並沒有繼承關係,因此ExtendedOperation無法共用BasicOperation中已定義的程式碼,本質上這實現了命令(Command)模式,如果真的有些想共用的程式碼,可考慮抽取出來成為一個輔助類別,或是靜態方法。
開放封閉原則
記得,列舉本身是用來定義一組有限的集合,有些元素天生適合列舉,有些則不是,有時這在一開始不容易判斷,但是,當需求變更,而開發者出現了擴充列舉成員的想法,甚至要修改對應的API實現時,就會是個訊號,因為,這個動作實際上也意謂著,目前的方式暴露了API的實現細節。
這時該思考的就是,這是否違反了開放封閉原則。也許需要的並不是列舉,而是其他的方案,像是組態物件,或者是為特定成員值實現不同的類別本體。
當然,在Java以外的語言中,可能不容易實現這樣的語法功能,此時,或許要進一步思考,還有必要堅持使用列舉型態嗎?或許,簡單地以命令模式實現,反倒是個適切的方案。