爲何使用枚舉做爲配置項(enum as configuration)是反開發模式的

翻譯自:Enums as configuration: the anti-patterngit

實現開閉原則

我常常看到有 Objective-C(偶爾也有 Swift)的設計中用到一種模式:使用枚舉類型(enum)做爲一個類的配置項。比方說,傳遞一個enumUIView來肯定一個顯示的樣式。在這篇文章裏,我會解釋爲何我認爲這種作法是反設計模式的,而且我會給出一個更強健、模塊化,擴展性更好的方式來解決這個問題。github

配置項帶來的問題

咱們先來看看枚舉到底會產生什麼問題。假設咱們有一個類用在不一樣的場景中,每個場景須要一個略微不一樣的配置項。因而在不一樣的場景下這個類的行爲應該也是不同的。這個類多是一個view,一個網絡客戶端類,或者其餘。類實現好了之後,用戶能夠指定或者根據不一樣的業務需求建立和配置這個類,而不須要去關心和修改這個類的任何實現細節。swift

提醒:接下來的例子用的是 Swift 3.0,可是對於 Objective-C 來講也是適用的。實際上咱們討論的這個話題對於任何語言都是適用的。設計模式

舉一個簡單熟悉的例子——UITableViewCell。假設咱們有個cell是由一張image、一組label和一個accessory view組成佈局的。因爲這個佈局有必定的通用性,因此咱們但願重用這個cell來顯示咱們App中不一樣的界面。比方說咱們給登陸視圖設計了特定顏色、字體等配置的cell。然而當咱們在設置視圖重用這個cell的時候,咱們但願其顏色、字體等配置是不一樣的。用到這個cell的界面須要這個cell下的subview的layout是差很少的,可是要有不一樣的視覺效果。緩存

用枚舉來配置

根據上文中的問題,咱們可能會設計下面這樣的代碼:網絡

enum CellStyle {
    case login
    case profile
    case settings
}

class CommonTableCell: UITableViewCell {
    var style: CellStyle {
        didSet {
            configureStyle()
        }
    }

    // ...

    func configureStyle() {
        switch cellStyle {
        case .login:
            // configure style for login view
            textLabel?.textColor = .red()
            textLabel?.font = .preferredFont(forTextStyle: UIFontTextStyleBody)

            detailTextLabel?.textColor = .blue()
            detailTextLabel?.font = .preferredFont(forTextStyle: UIFontTextStyleTitle3)

            accessoryView = UIImageView(image: UIImage(named: "chevron"))
        case .settings:
            // configure style for settings view
            textLabel?.textColor = .purple()
            textLabel?.font = .preferredFont(forTextStyle: UIFontTextStyleTitle1)

            detailTextLabel?.textColor = .green()
            detailTextLabel?.font = .preferredFont(forTextStyle: UIFontTextStyleCaption1)

            accessoryView = UIImageView(image: UIImage(named: "checkmark"))
         case .profile:
            // configure style for profile view
            // ...
        }
    }

    // ...
}

class SettingsViewController: UITableViewController {
   // ...

   func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
      // create and configure cell
      cell.style = .settings
      return cell
   }

   // ...
}
複製代碼

咱們建立了UITableViewCellUITableViewController的子類,而且定義了一個樣式的enum。而且在每一個不一樣的VC下建立cell後咱們設置了合適的樣式。很簡單,是吧?app

爲何枚舉的設計很爛

當設計一個庫或者框架的時候,「枚舉做爲配置項」的模式一般對用戶來講是提高了靈活性的——「看看給你提供的這些配置項!」。毫無疑問這是一個出於好意的設計,可是不要被其表象矇蔽了。咱們的目的是設計一個真正模塊化和適配性好的API,可是獲得的倒是一個有不少沒必要要的限制,難以維護而且很是容易出錯的結果。框架

這種設計模式「靈活」的緣由在於你能夠「設置任何你想要的樣式」,可是偏偏相反的是,枚舉自己的定義就是不靈活的——枚舉值的數量是有限的。在剛剛說到的例子當中就是,cell的樣式數量是有限的。若是你的App中有部分是這麼設計的話,每次你遇到一個新的場景須要用到這個cell,你須要增長一個caseCellStyle中而且更新那個龐大的switch語句。模塊化

若是這發生在一個庫中,用戶則沒有辦法去增長一個case到庫裏來定義他們本身的樣式。用戶不得不去給庫的做者發起一個pull request來增長一個枚舉項。更進一步說,即便是庫的做者給枚舉增長了一個項,從技術上來講對這個庫也是一個破壞性的改變——若是有一個用戶在程序的某個地方用switch語句用到了這個枚舉,這個時候編譯器就會提示語法錯誤,由於在 Swift 中 switch 語句必須是徹底的。佈局

而在 Objective-C 中的狀況會更糟糕——由於不徹底的switch語句不會報錯,很容易遇到忽略掉的break;並錯誤地走到下一個case中。固然,你能夠經過打開clang的一些警告配置-Wcovered-switch-default-Wimplicit-fallthrough-Wassign-enum-Wswitch-enum,來減小這些問題。可是我不認爲這樣就能解決問題。

這種方法脆弱且強制,會致使產生不少重複冗餘的代碼。咱們能夠處理得更好一些。

配置模型

與其被枚舉的種種問題折騰,咱們不如用一種被稱爲控制反轉(Inversion of Control,英文縮寫爲IoC)的設計模式來讓咱們的API更開放。繼續上面的例子,若是咱們建立一個全新的模型來表示咱們的cell樣式呢?代碼以下:

struct CellStyle {
    let labelColor: UIColor
    let labelFont: UIFont
    let detailColor: UIColor
    let detailFont: UIFont
    let accessory: UIImage
}

class CommonTableCell: UITableViewCell {
    // ...

    func apply(style: CellStyle) {
        textLabel?.textColor = style.labelColor
        textLabel?.font = style.labelFont

        detailTextLabel?.textColor = style.detailColor
        detailTextLabel?.font = style.detailFont

        accessoryView = UIImageView(image: style.accessory)
    }

    // ...
}
複製代碼

咱們用一個struct替代枚舉來表示咱們的cell樣式。這樣作不只僅清楚地定義了全部樣式的屬性,而且能夠用一種更簡潔、更聲明性的方式,將這些屬性直接映射到cell上。而且,咱們還能夠把這個struct類型做爲designated initializer的參數。

咱們已經從這個類中移除了成噸的複雜代碼,留下的只有更簡潔、易讀、易懂的代碼。有一個定義清晰,樣式屬性和cell的屬性一一對應的結構體,咱們不須要再維護那個巨大的switch語句,而且也不須要再面對其帶來的語法問題。同時,用戶不只僅可使用無限多的樣式,同時當有新的樣式需求時再也不須要去修改類自己的代碼,也不須要對封裝好的庫形成破壞性的改變。

默認和自定義屬性

這種設計更高級的另外一個緣由是咱們能夠以一種更純粹而且沒有破壞性的方式去設定默認值。Swift的一些特性在這裏簡直閃閃發亮——參數默認值、extensionstype inference。這門語言是如此的貼合這個設計模式,與之相比Objective-C就顯得笨重、乏味和冗餘了。

在Swift中,咱們能夠這樣設置默認值:

struct CellStyle {
    let labelColor: UIColor
    let labelFont: UIFont
    let detailColor: UIColor
    let detailFont: UIFont
    let accessory: UIImage

    init(labelColor: UIColor = .black(),
         labelFont: UIFont = .preferredFont(forTextStyle: UIFontTextStyleTitle1),
         detailColor: UIColor = .lightGray(),
         detailFont: UIFont = .preferredFont(forTextStyle: UIFontTextStyleCaption1),
         accessory: UIImage) {
        self.labelColor = labelColor
        self.labelFont = labelFont
        self.detailColor = detailColor
        self.detailFont = detailFont
        self.accessory = accessory
    }
}
複製代碼

對於用到的庫已經用枚舉來定義配置了,能夠用extension來這樣處理:

extension CellStyle {
    static var settings: CellStyle {
        return CellStyle(labelColor: .purple(),
                         labelFont: .preferredFont(forTextStyle: UIFontTextStyleTitle1),
                         detailColor: .green(),
                         detailFont: .preferredFont(forTextStyle: UIFontTextStyleCaption1),
                         accessory: UIImage(named: "checkmark")!)
    }
}

// usage:
cell.apply(style: .settings)
複製代碼

正如在前面提到的,用戶能夠經過增長一個extension更簡單地去獲得他想要的樣式。甚至他們還能夠選擇只重載其中的一部分默認屬性:

extension CellStyle {
    static var custom: CellStyle {
        // uses default fonts
        return CellStyle(labelColor: .blue(),
                         detailColor: .red(),
                         accessory: UIImage(named: "action")!)
    }
}
複製代碼

配置項做爲行爲

咱們以前的例子是集中在設置一個view的樣式,我須要強調的是這個強大的模式還能夠用與其餘的行爲。假設一個類用於響應網絡。這個類的配置項能夠指定協議、重連和失敗策略、緩存大小等等。在之前你可能定義一大串獨立的屬性,而如今你能夠把這些屬性打包到一個總體中,並提供默認值和容許自定義。

真實的案例

機智的讀者可能會想到,URLSessionURLSessionConfiguration不就是這麼設計的麼?這也是這個API能取代過期的NSURLConnection的緣由之一。咱們來看看URLSessionConfiguration提供的三個配置項:.default,.ephemeral,和.background(withIdentifier:)。它一樣容許你自定義屬性,想象一下若是用枚舉來設計的話侷限性會有多大。

咱們來看看另外一個例子——UIPresentationController。這個API讓咱們經過建立自定義的presentation controllers來定製VC的展現。之前這個API受限於其是用枚舉設計的。惟一能用的只有一個叫UIModelPresentationStyle的枚舉定義。正如咱們以前分析的,這對於用戶來講太不靈活了。可是UIKit並無在其新版的API裏100%地修復這個問題。仍然有部分的公共API依賴於UIModelPresentationStyle的值:

func adaptivePresentationStyle(for traitCollection: UITraitCollection) -> UIModalPresentationStyle
複製代碼

這個方法要求你返回一個UIModelPresentationStyle的值來指定UITraitCollection的樣式。咱們在這裏能作的僅僅就是隨意地返回一個UIModelPresentationStyle。若是你對這個例子感興趣,能夠在這裏找到我對這些API的研究.

最後一個例子,讓咱們看看 JSQMessagesViewController的升級進化。這個庫很老的一個版本中,提供了一個枚舉來決定時間戳在消息界面的顯示樣式,JSMessagesViewTimestampPolicy。而如今,在消息氣泡中的文本顯示方式顯示時機,是由一個data sourcedelegate來決定的。用戶不只僅能夠精確地肯定什麼時候顯示這些label,還能狗配置時間戳的顯示樣式。API僅僅是要求用戶配置一些文本就好了。你可能會注意到這個例子中並無用到咱們上面提到的配置項的struct對象。取而代之的是用了dataSourcedelegate來擔當這個角色——這正是咱們經過反轉控制的模式爲用戶提供更強大簡潔的API設定配置項的另外一種方法。

結論

這篇文章是open/closed principle(開閉原則) — the 「O」 in SOLID的一種實現。

軟件實體應當對擴展開放,對修改關閉。就是說,這個實體的源代碼能夠擴展,可是不能被修改。

咱們已經看到嘗試用枚舉的設計來實現這個原則對用戶來講限制頗多,而且易出錯切難以維護。可是使用配置項對象或者data sourcedelegate則能夠簡化代碼,杜絕錯誤且易於維護,同時提供了一個模塊化和可擴展的API給用戶,避免了破壞性的改變。 你的App能夠定製什麼類型的樣式、配置項或者行爲?能夠開始重構代碼啦。🤓

相關文章
相關標籤/搜索