为何使用枚举做为配置项(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能够定制什么类型的样式、配置项或者行为?能够开始重构代码啦。🤓

相关文章
相关标签/搜索