为什么使用枚举作为配置项(enum as configuration)是反开发模式的

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

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

实现开闭原则

我经常看到有 Objective-C(偶尔也有 Swift)的设计中用到一种模式:使用枚举类型(enum)作为一个类的配置项。比方说,传递一个enumUIView来确定一个显示的样式。在这篇文章里,我会解释为什么我认为这种做法是反设计模式的,并且我会给出一个更强健、模块化,扩展性更好的方式来解决这个问题。

配置项带来的问题

我们先来看看枚举到底会产生什么问题。假设我们有一个类用在不同的场景中,每一个场景需要一个略微不同的配置项。于是在不同的场景下这个类的行为应该也是不一样的。这个类可能是一个view,一个网络客户端类,或者其他。类实现好了以后,用户可以指定或者根据不同的业务需求创建和配置这个类,而不需要去关心和修改这个类的任何实现细节。

提醒:接下来的例子用的是 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后我们设置了合适的样式。很简单,是吧?

为什么枚举的设计很烂

当设计一个库或者框架的时候,“枚举作为配置项”的模式通常对用户来说是提升了灵活性的——“看看给你提供的这些配置项!”。毫无疑问这是一个出于好意的设计,但是不要被其表象蒙蔽了。我们的目的是设计一个真正模块化和适配性好的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可以定制什么类型的样式、配置项或者行为?可以开始重构代码啦。🤓

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容