為什么使用枚舉作為配置項(enum as configuration)是反開發(fā)模式的

因為https://blog.csdn.net/urdfmqcul2/article/details/78788962
,博客搬家至https://juejin.im/user/59fd6315f265da4321536990

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

實現(xiàn)開閉原則

我經(jīng)??吹接?Objective-C(偶爾也有 Swift)的設計中用到一種模式:使用枚舉類型(enum)作為一個類的配置項。比方說,傳遞一個enumUIView來確定一個顯示的樣式。在這篇文章里,我會解釋為什么我認為這種做法是反設計模式的,并且我會給出一個更強健、模塊化,擴展性更好的方式來解決這個問題。

配置項帶來的問題

我們先來看看枚舉到底會產(chǎn)生什么問題。假設我們有一個類用在不同的場景中,每一個場景需要一個略微不同的配置項。于是在不同的場景下這個類的行為應該也是不一樣的。這個類可能是一個view,一個網(wǎng)絡客戶端類,或者其他。類實現(xiàn)好了以后,用戶可以指定或者根據(jù)不同的業(yè)務需求創(chuàng)建和配置這個類,而不需要去關心和修改這個類的任何實現(xiàn)細節(jié)。

提醒:接下來的例子用的是 Swift 3.0,但是對于 Objective-C 來說也是適用的。實際上我們討論的這個話題對于任何語言都是適用的。

舉一個簡單熟悉的例子——UITableViewCell。假設我們有個cell是由一張image、一組label和一個accessory view組成布局的。由于這個布局有一定的通用性,所以我們希望重用這個cell來顯示我們App中不同的界面。比方說我們給登錄視圖設計了特定顏色、字體等配置的cell。然而當我們在設置視圖重用這個cell的時候,我們希望其顏色、字體等配置是不同的。用到這個cell的界面需要這個cell下的subview的layout是差不多的,但是要有不同的視覺效果。

用枚舉來配置

根據(jù)上文中的問題,我們可能會設計下面這樣的代碼:

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
   }

   // ...
}

我們創(chuàng)建了UITableViewCellUITableViewController的子類,并且定義了一個樣式的enum。并且在每個不同的VC下創(chuàng)建cell后我們設置了合適的樣式。很簡單,是吧?

為什么枚舉的設計很爛

當設計一個庫或者框架的時候,“枚舉作為配置項”的模式通常對用戶來說是提升了靈活性的——“看看給你提供的這些配置項!”。毫無疑問這是一個出于好意的設計,但是不要被其表象蒙蔽了。我們的目的是設計一個真正模塊化和適配性好的API,但是得到的卻是一個有很多不必要的限制,難以維護并且非常容易出錯的結(jié)果。

這種設計模式“靈活”的原因在于你可以“設置任何你想要的樣式”,但是恰恰相反的是,枚舉本身的定義就是不靈活的——枚舉值的數(shù)量是有限的。在剛剛說到的例子當中就是,cell的樣式數(shù)量是有限的。如果你的App中有部分是這么設計的話,每次你遇到一個新的場景需要用到這個cell,你需要增加一個caseCellStyle中并且更新那個龐大的switch語句。

如果這發(fā)生在一個庫中,用戶則沒有辦法去增加一個case到庫里來定義他們自己的樣式。用戶不得不去給庫的作者發(fā)起一個pull request來增加一個枚舉項。更進一步說,即使是庫的作者給枚舉增加了一個項,從技術上來說對這個庫也是一個破壞性的改變——如果有一個用戶在程序的某個地方用switch語句用到了這個枚舉,這個時候編譯器就會提示語法錯誤,因為在 Swift 中 switch 語句必須是完全的。

而在 Objective-C 中的情況會更糟糕——因為不完全的switch語句不會報錯,很容易遇到忽略掉的break;并錯誤地走到下一個case中。當然,你可以通過打開clang的一些警告配置-Wcovered-switch-default, -Wimplicit-fallthrough, -Wassign-enum, -Wswitch-enum,來減少這些問題。但是我不認為這樣就能解決問題。

這種方法脆弱且強制,會導致產(chǎn)生很多重復冗余的代碼。我們可以處理得更好一些。

配置模型

與其被枚舉的種種問題折騰,我們不如用一種被稱為控制反轉(zhuǎn)(Inversion of Control,英文縮寫為IoC)的設計模式來讓我們的API更開放。繼續(xù)上面的例子,如果我們創(chuàng)建一個全新的模型來表示我們的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的參數(shù)。

我們已經(jīng)從這個類中移除了成噸的復雜代碼,留下的只有更簡潔、易讀、易懂的代碼。有一個定義清晰,樣式屬性和cell的屬性一一對應的結(jié)構(gòu)體,我們不需要再維護那個巨大的switch語句,并且也不需要再面對其帶來的語法問題。同時,用戶不僅僅可以使用無限多的樣式,同時當有新的樣式需求時不再需要去修改類本身的代碼,也不需要對封裝好的庫造成破壞性的改變。

默認和自定義屬性

這種設計更高級的另一個原因是我們可以以一種更純粹并且沒有破壞性的方式去設定默認值。Swift的一些特性在這里簡直閃閃發(fā)亮——參數(shù)默認值、extensions、type 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
    }
}

對于用到的庫已經(jīng)用枚舉來定義配置了,可以用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的樣式,我需要強調(diào)的是這個強大的模式還可以用與其他的行為。假設一個類用于響應網(wǎng)絡。這個類的配置項可以指定協(xié)議、重連和失敗策略、緩存大小等等。在以前你可能定義一大串獨立的屬性,而現(xiàn)在你可以把這些屬性打包到一個整體中,并提供默認值和允許自定義。

真實的案例

機智的讀者可能會想到,URLSessionURLSessionConfiguration不就是這么設計的么?這也是這個API能取代過時的NSURLConnection的原因之一。我們來看看URLSessionConfiguration提供的三個配置項:.default,.ephemeral,和.background(withIdentifier:)。它同樣允許你自定義屬性,想象一下如果用枚舉來設計的話局限性會有多大。

我們來看看另一個例子——UIPresentationController。這個API讓我們通過創(chuàng)建自定義的presentation controllers來定制VC的展示。以前這個API受限于其是用枚舉設計的。唯一能用的只有一個叫UIModelPresentationStyle的枚舉定義。正如我們之前分析的,這對于用戶來說太不靈活了。但是UIKit并沒有在其新版的API里100%地修復這個問題。仍然有部分的公共API依賴于UIModelPresentationStyle的值:

func adaptivePresentationStyle(for traitCollection: UITraitCollection) -> UIModalPresentationStyle

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

最后一個例子,讓我們看看 JSQMessagesViewController的升級進化。這個庫很老的一個版本中,提供了一個枚舉來決定時間戳在消息界面的顯示樣式,JSMessagesViewTimestampPolicy。而現(xiàn)在,在消息氣泡中的文本顯示方式顯示時機,是由一個data sourcedelegate來決定的。用戶不僅僅可以精確地確定何時顯示這些label,還能狗配置時間戳的顯示樣式。API僅僅是要求用戶配置一些文本就行了。你可能會注意到這個例子中并沒有用到我們上面提到的配置項的struct對象。取而代之的是用了dataSourcedelegate來擔當這個角色——這正是我們通過反轉(zhuǎn)控制的模式為用戶提供更強大簡潔的API設定配置項的另一種方法。

結(jié)論

這篇文章是open/closed principle(開閉原則) — the “O” in SOLID的一種實現(xiàn)。

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

我們已經(jīng)看到嘗試用枚舉的設計來實現(xiàn)這個原則對用戶來說限制頗多,并且易出錯切難以維護。但是使用配置項對象或者data sourcedelegate則可以簡化代碼,杜絕錯誤且易于維護,同時提供了一個模塊化和可擴展的API給用戶,避免了破壞性的改變。
你的App可以定制什么類型的樣式、配置項或者行為?可以開始重構(gòu)代碼啦。??

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容