《创建型设计模式》之iOS系统框架实践

原文链接:http://qingmo.me/2017/03/11/creationalDesignPattern/
欢迎关注我的微博:http://weibo.com/shellhue

为了API的易用性、易维护性和健壮性,苹果工程师在iOS系统框架中其实运用了不少经典设计模式,而这些实践也正是因为良好的封装性,开发中我们虽日日相对,却也难以察觉它的存在。相对于其他生搬硬造隔靴搔痒的例子,这些我们熟悉的不能再熟悉的API方是学习设计模式的最佳案例。因此本系列拟以iOS系统框架中相关设计模式的实践为起点,探讨研究23种经典设计模式。

本文先讲述《创建型设计模式》(Creational Patterns)。

创建型设计模式是在创建一个类时用到的设计模式,总共有5种,其中工厂方法模式还可以根据实现的不同,分出简单工厂模式和工厂方法模式。

简单工厂模式(Factory Method)

iOS系统Foundation框架中的NSNumber所应用的就是简单工厂模式。

简单工厂模式主要解决不同情况下,需要创建不同子类,而这些子类又需要转化为公共父类让外界去使用的问题,因为这样对外接口只有一个,实际行为却因子类的具体实现而不同。拿NSNumber来说,传入IntFloatDoubleCharUnsignedChar等具体numberNSNumber返回的是对应的NSNumber子类,而我们使用时只知NSNumber,不知具体的子类。

import UIKit
let boolValue: Bool = true
let doubleValue: Double = 1.0

let boolN = NSNumber(value: boolValue)
let doubleN = NSNumber(value: doubleValue)

print(type(of:boolN))
print(type(of:doubleN))

输出结果为

__NSCFBoolean
__NSCFNumber

如果用简单工厂方法实现NSNumber(为了不与系统的NSNumber混淆,本文自己定义的NSNumber均去掉NS前缀,改为Number),代码大致如下:

// 抽象产品
protocol Number {
  func doubleValue() -> Double
  func boolValue() -> Bool
}

// 生产工厂
class NumberFactory {
  func createNumber(value: Bool) -> Number {
    return __NSCFBoolean(value: value)
  }
  func createNumber(value: Double) -> Number {
    return __CFNumber(value: value)
  }
}

// 具体的产品A
private class __CFBoolean: Number {
  let bool: Bool
  init(value: Bool) {
    bool = value
  }

  func doubleValue() -> Double {
    return bool ? 1 : 0
  }

  func boolValue() -> Bool {
    return bool
  }
}

// 具体的产品B
private class __CFNumber: Number {
  let double: Double
  init(value: Double) {
    double = value
  }

  func doubleValue() -> Double {
    return double
  }

  func boolValue() -> Bool {
    return double != 0
  }
}

其中Number是抽象协议,负责定义行为,而__CFNumber__CFBoolean是实现了Number抽象协议的私有实体类,NumberFactory则是一个创建Number的工厂。

具体使用时,先创建工厂,然后根据需要创建具体的实体类:

// 先创建工厂
let factory = NumberFactory()
// 然后根据需要创建实体类
let boolN = factory.createNumber(value: false)
let doubleN = factory.createNumber(value: 2.0)

而由于Objective-C的初始化方法中可以直接返回子类型,因此不必创建一个单独的工厂类NumberFactory,直接将相应的工厂方法逻辑封装在NSNumberinit方法中即可:

@implementation NSNumber
- (NSNumber *)initWithBool:(BOOL)value {
  return [[__NSCFBoolean alloc] initWithBool:value];
}
- (NSNumber *)initWithDouble:(double)value {
  return [[__NSCFNumber alloc] initWithDouble:value];
}
@end

而在Swift中不可能从init初始化方法中返回一个子类。(Swiftinit方法除了return nil外不能有返回值)

工厂方法模式(Factory Method)

简单工厂模式中,工厂只有一个实体类NumberFactory,每当添加新的产品(即新实现Number协议的子类),都需要去修改这个工厂。
比如上文新添加一个针对Float实现Number协议的__CFFloat(系统中的NSNumber并没有实体子类__NSCFFloat,而是所有的数字类型都封装为__NSCFNumber),

// 新添加的具体的产品C
private class __CFFloat: Number {
  let float: Float
  init(value: Float) {
    float = value
  }

  func doubleValue() -> Double {
    return Double(float)
  }

  func boolValue() -> Bool {
    return float != 0
  }
}

那么NumberFactory也需要改动:

// 生产工厂
class NumberFactory {
  func createNumber(value: Bool) -> Number {
    return __NSCFBoolean(value: value)
  }
  func createNumber(value: Double) -> Number {
    return __CFNumber(value: value)
  }
  // 新添加的工厂方法
  func createNumber(value: Float) -> Number {
    return __CFFloat(value: value)
  }
}

为解决这个弊端,可以将工厂NumberFactory也抽象一层,定义为一个协议:

// 抽象工厂
protocol NumberFactory {
  func createNumber(value: Any) -> Number
}

然后针对不同的Number实体子类,都定义相应的工厂NumberFactory子类即可:

// Bool 专用的工厂类
class BoolNumberFactory: NumberFactory {
  func createNumber(value: Any) -> Number {
    guard let value = value as? Bool else {
       fatalError("value must be a bool")
    }
    return __CFBoolean(value)
  }
}
// Double 专用的工厂类
class DoubleNumberFactory: NumberFactory {
  func createNumber(value: Any) -> Number {
    guard let value = value as? Double else {
       fatalError("value must be a double")
    }
    return __CFNumber(value)
  }
}

具体使用中,先创建工厂,然后直接根据相应的工厂创建相应的Number

// 先创建工厂
let boolFactory = BoolNumberFactory()
let doubleFactory = DoubleNumberFactory()

// 然后直接根据相应的工厂创建相应的Number
let boolN = boolFactory.createNumber(value: false)
let doubleN = doubleFactory.createNumber(value: 2.0)

如果想新添加一个针对Float实现Number协议的__CFFloat,添加完成后,直接再添加一个对应的NumberFactory子类即可。

// Float 专用的工厂类
class FloatNumberFactory: NumberFactory {
  func createNumber(value: Any) -> Number {
    guard let value = value as? Float else {
       fatalError("value must be a float")
    }
    return __CFFloat(value)
  }
}

这就是工厂方法模式与简单工厂模式的区别,即工厂方法模式不但抽象了产品,而且抽象了工厂。

抽象工厂模式(Abstract Factory)

工厂方法模式抽象了工厂,但只负责生产一种产品。抽象工厂模式与工厂方法模式一般无二,都是抽象了工厂和产品,只是抽象工厂模式中的抽象工厂会负责生产一种以上相关联、会一起使用的产品。
还是以Number的抽象工厂NumberFactory举例。Foundation中类似NSNumber类簇的,还有NSArray:

import UIKit
let array0 = NSArray(array: [])
let array1 = NSArray(arrayLiteral: 1, 2)
let array2 = NSArray(arrayLiteral: 1)

print(type(of:array0))
print(type(of:array1))
print(type(of:array2))

打印结果如下:

__NSArray0
__NSArrayI
__NSSingleObjectArrayI

定义NSArray的抽象协议并实现两个私有类__CArray0__CArrayI,为不与系统中的NSArrayArray混淆,这里取CArray

protocol CArray {
  var count: Int { get }
  // 其他公共接口
}
private class __CArray0: CArray {
  var count = 0
  // 其他公共接口实现

}
private class __CArrayI: CArray {
  var count = 0
  // 其他公共接口实现
}

定义NumberCArray的抽象工厂协议NumberAndArrayFactory

protocol NumberAndArrayFactory {
  // 用来生产Number的工厂方法
  func createNumber(value: Any) -> Number
  // 用来生产CArray的工厂方法
  func createCArray() -> CArray
}

定义抽象工厂NumberAndArrayFactory的具体实现类BoolNumberAndArray0Factory:

class BoolNumberAndArray0Factory: NumberAndArrayFactory {
  func createNumber(value: Any) -> Number {
    guard let value = value as? Bool else {
       fatalError("value must be a bool")
    }
    return __CFBoolean(value)
  }
  
  func createCArray() -> CArray {
    return __CArray0()
  }
}

具体使用时,先创建抽象工厂NumberAndArrayFactory的具体实现类,然后在调用这个实现类上的工厂方法,创建相应的产品Number或者CArray

// 创建抽象工厂`NumberAndArrayFactory`的具体实现类
let boolNumberAndArray0Factory = BoolNumberAndArray0Factory()

// 调用工厂方法,创建相应的产品
let boolNumber = boolNumberAndArray0Factory.createNumber(true)
let 0CArray = boolNumberAndArray0Factory.createCArray()

需要注意的是,这里为了说明抽象工厂模式,抽象工厂NumberAndArrayFactory所创建的NumberCArray没有任何关联,在实际项目中,同一抽象工厂所创建的产品是关联的,一般是一起结合使用,如果不关联,也不必用抽象工厂模式。

建造者模式(builder)

建造者模式是用来隔离复杂对象的配置过程,将复杂对象的配置过程单独封装成一个builder对象,完成配置后,再获取配置完成的实例对象。

cocoa中使用建造者模式的类是NSDateComponents,

import Foundation

var builder = NSDateComponents()

builder.hour = 10
builder.day = 6
builder.month = 9
builder.year = 1940
builder.calendar = Calendar(identifier: .gregorian)

var date = builder.date
print(date!)

输出结果为:

1940-09-06 01:00:00 +0000

NSDateComponents相当于日期的一个builderNSDateComponents用来配置日期的各个部分,配置完成后,最终获取对应的Date日期。
NSDateComponents的实现大致如下(为避免与系统中的NSDateComponentsDateComponents混淆,这里取DateBuilder):

class DateBuilder {
  var hour = 0
  var day = 0
  var month = 0
  var year = 1970
  var calendar = Calendar(identifier: .gregorian)

  var date: Date {
    // 根据date components计算日期,比较复杂,这里省略了计算过程
    let calculatedDate = ...
    return calculatedDate
  }  
}

但这使用上不能链式调用,很不方便,加上各个属性的设置方法,返回自己本身,可以实现链式调用:

class DateBuilder {
  var hour = 0
  var day = 0
  var month = 0
  var year = 1970
  var calendar = Calendar(identifier: .gregorian)

  var date: Date {
    // 根据DateComponents计算日期,比较复杂,这里省略了计算过程
    let calculatedDate = ...
    return calculatedDate
  }
  func hour(_ hour: Int) -> DateBuilder {
    self.hour = hour
    return self
  }
  func day(_ day: Int) -> DateBuilder {
    self.day = day
    return self
  }
  func month(_ month: Int) -> DateBuilder {
    self.month = month
    return self
  }
  func year(_ year: Int) -> DateBuilder {
    self.year = year
    return self
  }
  func calendar(_ calendar: Calendar) -> DateBuilder {
    self.calendar = calendar
    return self
  }
}

使用时很方便:

let date = DateBuilder().hour(1).day(2).month(12).year(2017).date

原型模式(Prototype)

原型模式其实就是一个类能够通过自身copy,创建一个内容一模一样的新实例,这在iOS的系统框架Foundation中挺常见的,NSStringNSArrayNSDictionaryNSParagraphStylecopymutableCopy方法都能复制一个新的实例,从而免去了从零创建一个复杂类的麻烦。
NSParagraphStyle,当获取到一个paragraphStyle之后,突然又想在其基础上改动同时又不想直接改变原来NSParagraphStyle,最方便的不过copy一份原来的,然后在改动。

let paragraphStyle = NSParagraphStyle.default
let mutablePara = paragraphStyle.mutableCopy() as! NSMutableParagraphStyle
mutablePara.lineSpacing = 10
mutablePara.paragraphSpacing = 5

如果想实现原型模式,在swift中直接实现NSCopyingNSMutableCopying协议即可。

单例模式(Singleton)

单例模式即一个类至始至终只有一个实例(单例类是可以新创建实例的,但一般都会用公共的那个单例实例),常用于Manager上。单例在iOS系统中十分常用,如NSParagraphStyle.defaultUIScreen.mainUIApplication.sharedUserDefaults.standardFileManager.default等都是单例。

let paragraphStyle = NSParagraphStyle.default
let screen = UIScreen.main
let application = UIApplication.shared
let userDefault = UserDefaults.standard
let fileManager = FileManager.default

在swift中实现一个单例模式,也是非常简单的。

class Manager {
  // 单例
  static let shared = Manager()
  // 私有化后,这个对象只会有单例这一个实例
  private init() {

  }
}

上述单例,初始化方法私有化了,因此在整个APP的生命周期中,将只有一个此类的实例,即单例。
但有时,单利只是给一个默认配置而已,如果想自定义,可以完全重新初始化一个新的实例,如

class ParagraphStyle {
  // 单例
  static let default = ParagraphStyle()
  // 没有私有化,这个对象如果有需要可以创建单例以外的新的实例
  init() {

  }
}

总结

创建型设计模式在iOS系统中的运用相当广泛,而我们开发中只要有一定的抽象,基本都会用到,尤其是简单工厂模式和工厂模式、单例模式,希望本文的讲解能让大家能真正理解这些开发模式,并在开发中顺利应用。

欢迎关注我的微博:http://weibo.com/shellhue

参考文章:

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

推荐阅读更多精彩内容

  • 设计模式汇总 一、基础知识 1. 设计模式概述 定义:设计模式(Design Pattern)是一套被反复使用、多...
    MinoyJet阅读 3,875评论 1 15
  • Iterator模式 (迭代器) 一个一个遍历 一个集合类可以遵守 Iterator 协议,并实现一个 Itera...
    SSBun阅读 1,817评论 0 15
  • 1 场景问题# 1.1 选择组装电脑的配件## 举个生活中常见的例子——组装电脑,我们在组装电脑的时候,通常需要选...
    七寸知架构阅读 4,223评论 6 66
  • 设计模式基本原则 开放-封闭原则(OCP),是说软件实体(类、模块、函数等等)应该可以拓展,但是不可修改。开-闭原...
    西山薄凉阅读 3,723评论 3 13
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,497评论 18 399