设计模式-创建型

DinS          Written on 2017/11/10

本文介绍设计模式相关内容,如果读者对OOP还不太了解,建议参考《我们为什么需要面向对象编程?》及其后续文章。

何谓设计模式?设计模式(Design Pattern)可以看作解决某一类问题的特别巧妙和具有洞察力的方法。说的再通俗一点,就是解决某一类常见问题的套路。
提到设计模式不可避免提到四个人:Gamma,Helm,Johnson,Vlissides,这四人合成“四人帮”(Gang of Four, GoF)。他们著作了一本书《设计模式》,这也是设计模式概念的来源。
这本书最好单独阅读。本节以《C++编程思想(第二卷)》第十章为蓝本,介绍常用的设计模式,同时结合上述水果商人的例子进行具体讲解。

本文介绍创建型设计模式,其用于处理如何创建一个对象。
其用意是隔离创建对象的细节,这样代码就不依赖于对象的具体类型,因此在增加一种新对象类型时不需要改变代码。
OOP中对象创建可谓是家常便饭,所以创建型设计模式应该是最常用的了。

一、工厂(factory)模式

刚刚在水果商人的程序中已经出现了一些问题,就是在创建对象时我们写了多个if-else,根据输入信息创建不同类型的智能指针。这是有隐患的,因为我们把Fruit的派生类引入了高一级的抽象层次。
创建对象时必须要指定准确的构造函数,如果我们在所有创建对象的地方都用if-else,那么很快这些代码会遍布全部工程。一旦新增加了一个派生类,我们要在工程中所有创建类的地方都做修改,这是一个灾难。

为了解决这个问题,我们引入了工厂的概念。将创建对象的代码全部转移到工厂中进行,这样将来增加新类型时只需要修改工厂即可。
工厂模式可以称之为最有用的设计模式之一,也正因为此它有三种形态,以应对不同的创建对象需求。

1.简单工厂

这个大概是最好理解、最易使用的工厂模式。方法就是在基类中定义一个静态成员函数。

大图点这里

等等,什么叫做静态成员函数?
静态是类的一个高级用法,之前在讲解数据抽象时特意没有说,避免复杂。现在读者已经轻车熟路了,再来介绍。

所谓静态成员,就是跟类本身相关,跟类的对象无关的成员。使用static来声明静态成员变量或函数。所以静态就是static的翻译。对于静态成员变量,要在类外部定义,即使该成员在类内部被初始化了也要如此。
如何使用静态成员呢?很简单,既然静态成员跟对象无关,只跟类本身有关,那么我们就直接调用类来使用,语法是类名::静态成员。

比如这里可以这样使用MakeFruit:
Fruit::MakeFruit(…)
可见没有什么神秘的。大多数情况下用不到静态成员,不过有的时候使用静态成员可以有奇效。

来看看这个MakeFruit的实现:

大图点这里

很整齐,很简单。不需要过多解释了。
虽然简单,但是可以让代码变得更美。
来看之前的Basket的构造对象的代码:

使用了很长的判断来生成不同派生类对象,而且一旦添加新种类我们必须改动程序中的每一处构造对象代码。现在借助工厂模式可以大大简化:

一行代码解决。而且更重要的是,这样一来Basket中彻底没有了Fruit的派生类。
在创建对象的时候统一使用工厂模式,这样一旦新增加类型我们只需要在工厂函数中添加新类型即可,构造对象的代码完全不用动。

但是简单工厂有一个缺点:基类必须知道所有派生类。
随着系统的扩展,基类的工厂方法必须不断更新,加入新派生类。这会让系统变得笨拙,逻辑也会变得复杂。

2.多态工厂(polymorphic factory)

为了解决简单工厂的问题,提出了多态工厂模式:

其思想是建立一个工厂类作为基类,从这个基类派生出各种具体的工厂,每一种具体的工厂创建一种具体的产品,存在一一对应关系。于是在建立对象时我们给出要创建的工厂,然后利用该工厂得到具体的产品。

多态工厂最大的优点是新添加派生类时无需修改工厂类,只要派生出对应的具体工厂即可,缺点就是产品和工厂需要一对一出现,增加了系统复杂度。

实际上在这里我们又一次看到了一条普遍真理:应对变化的能力以复杂度增加为代价。如果能够用简单工厂解决,就不要用多态工厂

3.抽象工厂(abstract factory)

刚刚介绍的都是创建单一类体系下对象的方法,如果我们想创建出不同类体系下的不同对象,然后让这些对象之间互动,应该如何呢?可以使用抽象工厂来解决。
先来回忆一下之前设计的类体系:

这个体系是分层式的,Coupon从属于Fruit,或者这样说,在Fruit要计算总价时才创建出Coupon。但是我们可以换个角度想问题:如果把Fruit和Coupon提升为同一个抽象层次,会如何呢?
先分开思考这两个类体系:

这没什么稀奇的,我们在讨论的可是抽象工厂,这个东西也是一个类体系:

Bundle是对Fruit和Coupon的组合,也就是说一个Bundle里面有一种Fruit和一种Coupon,一旦有了这两个我们可以写一个函数令二者相互作用,得到总价。
BundleFactory就是制造Bundle的工厂类,即抽象工厂,从其派生出来的是各种具体Fruit和Coupon的组合。

注意,一旦这样来设计,那么之前我们设计的类体系就不能沿用了。我新建了一个工程并修改了之前的代码,要点如下:
Coupon类不变
Fruit类有较大变化

大图点这里

不再保留折扣券类型的成员变量,改为私有函数,传入一个折扣券。计算总价时也要传入折扣券。

看实现:

虽然只改了两个函数,但是在设计思路上是巨大转变。
这样设计后水果和折扣券是并行的两个概念,二者独立变化不影响其他的使用。但是同时也把折扣券显式地引入了水果,即我们在计算总价时必须提供一个折扣券智能指针。
如何把这个东西做好呢?那就是依靠抽象工厂和辅助类。
抽象工厂的思路上面已经给出,实现如下:

基类的抽象工厂,只有两个虚函数用于创造Fruit和Coupon对象。

具体工厂直接根据类型创建了对象。

辅助类Bundle实现如下:

大图点这里

这个辅助类的要点在于构造时参数里有一个工厂,根据这个工厂我们构造出希望获得的某类型对象,然后利用这两个对象去进行下一步操作。

使用时如下:

我知道抽象工厂理解起来有些难度,所以再补几张图来说明。
第一张是类体系图:

我们直接使用的是Bundle类,但是这个类中起关键性作用的有三个东西:m_pFruit,m_pCoupon和pBundleFactory。
这三个东西都是基类,也就是说在程序运行时实际上指向的会是派生类。

那么实际运行时具体的过程如何呢?

大图点这里

这里面深入了好几层,所以难以理解。
这么复杂,我们获得的好处是什么呢?
首先,我们把水果和折扣券进行了拆分,现在是平等关系,二者变化起来更加灵活。
其次,我们将几个类体系有机融合在一起了,而且对于客户程序员而言不需要知道内部的具体细节就可以直接使用。
最后,通过抽象工厂我们可以不断派生出各种组合方式,并且不需要动之前的代码,另外基类也不需要知道派生类的具体情况,这点远远优于简单工厂。

当然缺点还是明显的,复杂了是肯定的,更重要的是具体工厂的数量可能急剧膨胀。如果水果数量是m,折扣券供应商数量是n,那么有可能需要派生出m*n种工厂,如果再加入t种其他的类,比如地区,那么就会很吓人。
这构成了抽象工厂的一个重要特点:增加新的产品组很方便,但是增加新的产品种类很麻烦。
决定是否使用抽象工厂时需要考虑这一点。

现在我们已经有了两种水果商人的类体系设计,哪种更加好呢?
虽然这看起来是个仁者见仁智者见智的事,但是类体系设计是有一些公认的评价标准的。这个留待最后再揭晓。
这里想表达的是希望读者理解两个类体系的本质区别:
第一个类体系中,Coupon被作为Fruit的下级概念使用,因此整个体系呈现出等级制
第二个类体系中,Coupon和Fruit是并列的两个概念,通过组合的方式相互作用

二、单件模式(singleton)

这个用的也是很多的,其用意是确保一个类只有一个对象。

一般而言使用静态成员变量+静态成员函数的方式创建、获取这个单件。

网络上有很多示例,这里就不介绍了。

三、构建器(builder)模式

builder模式比工厂模式更加复杂,是专门用于创建复杂对象的,其用意是解决这样一个基本的问题:
现有一批零部件,使用不同方法组装将得到不同产品。组装方法可能变化。我们想要得到的是拼接好的产品,同时不改变原有代码。

这个东西很复杂,使用频率较低。
如果在项目中遇到要创建复杂对象(有不同部分拼接而成)同时又希望保持灵活性,那么可以自行搜索builder模式。

四、小结

创建型的设计模式是运用最广泛的。
在OOP中至少应该用到简单工厂,这是性价比最高的。
如果涉及了多个类体系之间不同产品的组合,可以考虑使用抽象工厂。
如果涉及到更复杂的对象构造,可以考虑使用builder模式。
但还是那句话:灵活性以复杂度增加为代价,如果可以就做最简单的。

至于其他设计模式,可见《OOP实例-水果商人(二)》文末。