设计模式之创建型模式

引言

GoF的《设计模式》一书总结了面向对象软件中一些宝贵的设计经验,系统地对它们命名、解释和评价,并以编目分类的形式将它们展现出来,这就是广为流传的23个设计模式的由来。

模式是一项管理复杂度的技术,几乎所有模式都遵循两个原则:

  • 针对接口编程,而不是实现。
  • 多用组合,少用继承。

很多模式看上去很类似,这是因为实现各种模式的方式一般就是继承和组合。对外暴露一个通用接口,既易于使用又隐藏实现细节,内部用各种子类来实现不同功能,支持扩展变化,并尽量用对象组合来实现解耦。所以你可以认为23个模式就是根据不同的使用场景变着法儿地声明接口然后继承实现最后再花式组合罢了。

模式依据其设计目的可以分为三大类——创建型(Creational)、结构型(Structural)和行为型(Behavioral)。本文主要论述几个创建型模式之间的区别与联系。

《设计模式》一书的副标题是“可复用面向对象软件的基础”,所以显然这23个设计模式是用于面向对象软件设计的,而众所周知,最适合面向对象这种范式的领域其实是 GUI 编程领域(这也是《设计模式》中大部分应用实例都是一些 GUI 框架的原因),所以本文也主要以 iOS 开发为例进行说明。示例语言选用静态语言 Swift(其实个人认为设计模式主要还是针对静态语言,很多模式在动态语言中都用处不大)。

创建型模式简介

创建型模式将实例化对象的部分从系统中独立出来,它们将系统具体使用哪些类的信息封装起来,并隐藏了这些类是如何被创建和组合的,对外只提供一个通用接口。

创建型模式有五种——Abstract Factory(抽象工厂)、Builder(生成器)、Factory Method(工厂方法)、Prototype(原型)、Singleton(单例)。我个人认为抽象工厂模式和生成者模式的抽象层级最高,因为它们都可以分别用工厂方法和原型实现。而工厂方法和原型是同一个层级的,它们在大多数时候是互斥的,一般不能结合使用。至于单例,就是保证某个类只实例化一次而已,想用在哪儿都行(只要符合实际需求)。

抽象工厂侧重于创建一系列同一风格的产品,每个产品都有一个抽象接口,使用者并不知道它使用的是具体哪种风格的产品。而生成器侧重于一步步构建一个复杂产品,这个复杂产品不需要有一个公共接口,使用者知道它具体得到了一个什么产品。

抽象方法定义一个用于创建对象的接口,子类重写创建方法,被创建的产品会有一个抽象接口,所以使用者并不知道具体得到的是什么产品。原型将某个实例对象作为“原型”,通过复制这个原型来创建新的对象,由于可以动态指定原型,所以可以在运行期改变创建的产品。

一个简单案例

假设我们现在要构建两个界面,界面的构成元素都是一个 Label 和 Button。一个界面在打开应用的时候显示,Label 和 Button 会显示“Hello……”,另一个在应用关闭前显示,Label 和 Button 会显示“Goodbye……”。而且不止是显示的文字,连同背景色、位置、大小等等属性都会不同。于是我们考虑自定义几个 Label 和 Button:

//MARK: - Hello 系列产品
class HelloButton: UIButton {
    init() {
        let frame = CGRect(x: 50, y: 200, width: 300, height: 50)
        super.init(frame: frame)
        backgroundColor = UIColor.greenColor()
        setTitle("Hello, I am a button.", forState: .Normal)
        setTitleColor(UIColor.redColor(), forState: .Normal)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
}

class HelloLabel: UILabel {
    init() {
        let frame = CGRect(x: 50, y: 300, width: 300, height: 50)
        super.init(frame: frame)
        backgroundColor = UIColor.yellowColor()
        text = "Hello, I am a label."
        textAlignment = .Center
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}


//MARK: - Goodbye 系列产品
class GoodbyeButton: UIButton {
    init() {
        let frame = CGRect(x: 50, y: 400, width: 300, height: 20)
        super.init(frame: frame)
        backgroundColor = UIColor.redColor()
        setTitle("Goodbye, don't forget I'm a button.", forState: .Normal)
        setTitleColor(UIColor.greenColor(), forState: .Normal)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
}

class GoodbyeLabel: UILabel {
    init() {
        let frame = CGRect(x: 100, y: 500, width: 200, height: 100)
        super.init(frame: frame)
        backgroundColor = UIColor.blackColor()
        text = "Goodbye, don't forget I'm a label."
        textColor = UIColor.whiteColor()
        font = UIFont.systemFontOfSize(10)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

然后我们定义一个用来组合界面视图的类:

class PageView: UIView {
    init() {
        super.init(frame: UIScreen.mainScreen().bounds)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

稍后我会介绍用不同的模式来创建产品。

Factory Method(工厂方法)

现在我们给 PageView 加上用来创建 Label 和 Button 的工厂方法,并在构造器中调用工厂方法。工厂方法可以是抽象方法也可以有一个默认实现,这里我给出一个默认实现:

class PageView: UIView {
    init() {
        super.init(frame: UIScreen.mainScreen().bounds)
        
        let label = createLabel()
        let button = createButton()
        
        addSubview(label)
        addSubview(button)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func createLabel() -> UILabel {
        return UILabel()
    }
    
    func createButton() -> UIButton {
        return UIButton()
    }
}

这样我们要创建 HelloPageView 和 GoodbyepageView 的时候只要重写工厂方法就好了:

class HelloPageView: PageView {
    override func createLabel() -> UILabel {
        return HelloLabel()
    }
    override func createButton() -> UIButton {
        return HelloButton()
    }
}

class GoodbyePageView: PageView {
    override func createLabel() -> UILabel {
        return GoodbyeLabel()
    }
    override func createButton() -> UIButton {
        return GoodbyeButton()
    }
}

常规的工厂方法有个显而易见的缺点就是当需要进行不同产品的组合的时候,容易导致类爆炸。譬如现在我们只是需要“HelloLabel + HelloButton”和“GoodByeLabel + GoodbyeButton”,但如果我们还需要“ HelloLabel + GoodbyeButton”和“ GoodByeLabel + HelloButton”这样的组合,那就又得新建两个 PageView 的子类。

解决这个问题的方法是可以使用参数化的工厂方法,可以给工厂方法传递一个参数(标识符),然后根据标识符来实例化特定的产品,这样我们就不需要各种 PageView 子类了。但是一旦扩充了新产品(增加了新的XXXLabel或者XXXButton),就得去修改相应的工厂方法以支持新产品。这时候如果是支持范型的语言,就可以使用范型参数来解决这个问题(前提是工厂方法中没有针对某个特定子类产品的操作),我们把 PageView 改成一个范型类:

class PageView<L: UILabel, B: UIButton>: UIView {
    init() {
        super.init(frame: UIScreen.mainScreen().bounds)
        
        let label = createLabel()
        let button = createButton()
        
        addSubview(label)
        addSubview(button)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func createLabel() -> L {
        return L()
    }
    
    func createButton() -> B {
        return B()
    }
}

这样就可以在 Controller 中指定要返回那种类型的 PageView,可以任意组合 Label 和Button:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        let pageView = PageView<HelloLabel, HelloButton>()
        view.addSubview(pageView)
    }
}

效果如下:

HelloPage.png

若要使用 GoodPage,只需要把PageView<HelloLabel, HelloButton>()换成PageView<GoodbyeLabel, GoodbyeButton>(),运行效果就变成:

GoodbyePage.png

Prototype(原型)

原型模式顾名思义就是将某个实例对象当做原型,通过复制它来创建其他同类型的对象。要使用原型模式需要给产品类设置一个用来克隆自身实例的函数,虽然很多语言或者标准库都有 copy 函数,对克隆对象提供了一些原生支持,但你还得考虑深拷贝和浅拷贝的问题,前者同时拷贝对象内部的状态,后者则通过指针共享状态。

像 Self、JavaScript 这样基于原型的语言可以说处处都用到了原型模式,而像SmallTalk、OC、Ruby 等动态语言中,类本身可以当作对象传递并用其创建实例对象,甚至在 Swift 中也可以直接用所谓的元类型(SomeClass.Type)来初始化一个对象,所以我觉得这个原型模式在很多时候并不实用。它最大的优点是灵活性,可以动态指定要创建的对象,而这点,可以通过传递“元类型”或者利用范型轻易做到。

Abstract Factory(抽象工厂)

抽象工厂通常是利用工厂方法来实现的,也可以利用范型或者原型。它的核心思路就是单独抽象出一个工厂类,通过对象组合,系统委托这个工厂类来创建一系列产品。没错,这个模式的重点就在于强调了“一系列”这三个字。如果你的最终目的是要把这一系列产品组合成一个产品,那就应该用 Builder 模式。

所以我上面举的那个实例其实用 Builder 模式比较合适,当然,我这里也可以强行用抽象工厂做一下,只要把最后组装产品那一步留到外部好了。

//MARK: - 抽象工厂
protocol UIFactory {
    func createLabel() -> UILabel
    func createButton() -> UIButton
}

//MARK: - 具体工厂
class HelloUIFactory: UIFactory {
    func createLabel() -> UILabel {
        return HelloLabel()
    }
    
    func createButton() -> UIButton {
        return HelloButton()
    }
}

class GoodbyeUIFactory: UIFactory {
    func createLabel() -> UILabel {
        return GoodbyeLabel()
    }
    
    func createButton() -> UIButton {
        return GoodbyeButton()
    }
}

然后修改 PageView,构造器以一个 UIFactory 对象为参数:

class pageView: UIView {
    init(factory: UIFactory) {
        super.init(frame: UIScreen.mainScreen().bounds)
        
        let label = factory.createLabel()
        let button = factory.createButton()
        
        addSubview(label)
        addSubview(button)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

如果要生成一个 HelloPaveView 并显示,只需要在 Controller 的 ViewDidLoad 方法中这样写就好了:

let pageView = PageView(factory: HelloUIFactory())
view.addSubview(pageView)

当然跟工厂方法一样,为了避免类继承层次过深,也可以使用范型版本的工厂,只不过这样在使用的时候就需要明确指出需要创建的产品类型了:

//用范型控制产品类型,不需要定义一堆 UIFactory 子类。
class GenericUIFactory<L: UILabel, B: UIButton>: UIFactory { 
    func createLabel() -> UILabel {
        return L()
    }
    
    func createButton() -> UIButton {
        return B()
    }
}

使用:

let pageView = PageView(factory: GenericUIFactory<HelloLabel, HelloButton>())

Builder(生成器)

生成器模式同样需要用到对象组合,director 对象委托 builder 对象一步步构建出一个复杂对象。先定义一个 Director 类:

class Director {
    func createPageViewWithBuilder(builder: Builder) {
        builder.createButton()
        builder.createLabel()
    }
}

Builder 定义如下:

protocol Builder {
    func createLabel()
    func createButton()
}

注意这个 Builder 的协议并没有声明返回产品的方法,因为用 Builder 构造出来的产品往往差别很大,并没有一个统一的接口,所以只需要在子类中声明一个返回特定产品的方法即可(当然本例中其实最终构造的产品都是UIView,是可以提供一个统一接口的)。下面是 Builder 子类实现:


class HelloPageBuilder: Builder {
    var helloPageView: UIView
    
    init() {
        helloPageView = UIView()
    }
    
    func createLabel() {
        helloPageView.addSubview(HelloLabel())
    }
    
    func createButton() {
        helloPageView.addSubview(HelloButton())
    }
    
    func getHelloPageView() -> UIView {
        return helloPageView
    }
}

class GoodbyePageBuilder: Builder {
    var goodbyePageView: UIView
    
    init() {
        goodbyePageView = UIView()
    }
    
    func createLabel() {
        goodbyePageView.addSubview(GoodbyeLabel())
    }
    
    func createButton() {
        goodbyePageView.addSubview(GoodbyeButton())
    }
    
    func getGoodbyePageView() -> UIView {
        return goodbyePageView
    }
}

使用:

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        let director = Director()
        let builder = HelloPageBuilder()
        director.createPageViewWithBuilder(builder)
        let pageView = builder.getHelloPageView()
        view.addSubview(pageView)
    }
}

上述的 Builder 也用了工厂方法来实现,同样你可以根据实际情况使用范型或者原型模式来实现。

Singleton(单例)

单例实在是太简单了,无非就是确保只实例化某个类一次。在某些语言中,使用单例时得注意线程安全。但在 Swift 中,只要使用let声明一个常量,用它指向一个实例,它的 immutable 性质可以保证线程安全,然后把对应的构造器设为 private 就可以了,像这样:

private let instance = HelloUIFactory()
class HelloUIFactory: UIFactory {

    private init() {}
    
    class func singleInstance() -> HelloUIFactory {
        return instance
    }
    
    func createLabel() -> UILabel {
        return HelloLabel()
    }
    
    func createButton() -> UIButton {
        return HelloButton()
    }
}

有一点需要注意,Swift 的private关键字的作用域是以文件为单位的,而不是类,所以虽然把HelloUiFactory类的构造器声明为private了,但在本文件内(哪怕在HelloUiFactory类外部),还是可以实例化该类。

使用单例:

let factory = HelloUIFactory.singleInstance()

一般像 factory 啊 builder 啊 prototype 啊等等其实都只要一个实例就够了,所以你喜欢的话很多地方都可以用单例模式。

后记

创建型模式就介绍到这里,接下来会再写一篇结构型模式介绍和一篇行为型模式介绍。可能你觉得很多模式平常根本用不到,没关系,理解并就行了。等哪天你看别人的源码的时候看着看着福至心灵:“耶?这不是XXX模式么?”,或者等你的项目规模大到一定程度的时候脑中灵光一闪:“这里用XXX模式似乎不错~”的时候,你就知道模式有什么用了。当然,时代在发展,很多现代语言或者标准库已经集成了一些模式,不需要自己费力去实现了。还有很多场景,哪怕可以使用模式也需要进行一定的变通,不要照搬照抄、强行套用。

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

推荐阅读更多精彩内容